feat(M13): get_worker_settings + worker_heartbeat tools (v0.7.0)
T-519 — pre-flight quota-gate voor de worker-loop. Twee nieuwe MCP-tools: - get_worker_settings (read): retourneert User.min_quota_pct. Worker roept dit elke iteratie aan vóór de quota-probe. - worker_heartbeat (write): worker rapporteert last_quota_pct + last_quota_check_at na een probe. Update ClaudeWorker en emit pg_notify 'worker_heartbeat' op scrum4me_changes-channel zodat NavBar stand-by-badge real-time updatet. requireWriteAccess (demo-blok). Schema-resync: vendor/scrum4me bijgewerkt naar 555ed8f waarmee de M13-velden (User.min_quota_pct, ClaudeWorker.last_quota_pct + last_quota_check_at) beschikbaar zijn voor Prisma client. Bestaande achtergrond-heartbeat (presence/heartbeat.ts, 5s tick op last_seen_at) blijft ongewijzigd. Worker_heartbeat is een aparte expliciete call met quota-info. Versie naar 0.7.0 (minor — twee nieuwe tools). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b48f2a5c74
commit
d50075d960
6 changed files with 192 additions and 8 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "scrum4me-mcp",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.0",
|
||||
"description": "MCP server for Scrum4Me — exposes dev-flow tools and prompts via the Model Context Protocol",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ enum Role {
|
|||
PRODUCT_OWNER
|
||||
SCRUM_MASTER
|
||||
DEVELOPER
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum StoryStatus {
|
||||
|
|
@ -32,6 +33,7 @@ enum ClaudeJobStatus {
|
|||
DONE
|
||||
FAILED
|
||||
CANCELLED
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
enum VerifyResult {
|
||||
|
|
@ -85,6 +87,7 @@ enum ClaudeJobKind {
|
|||
TASK_IMPLEMENTATION
|
||||
IDEA_GRILL
|
||||
IDEA_MAKE_PLAN
|
||||
PLAN_CHAT
|
||||
}
|
||||
|
||||
enum IdeaLogType {
|
||||
|
|
@ -96,6 +99,11 @@ enum IdeaLogType {
|
|||
JOB_EVENT
|
||||
}
|
||||
|
||||
enum UserQuestionStatus {
|
||||
pending
|
||||
answered
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
|
|
@ -104,10 +112,12 @@ model User {
|
|||
is_demo Boolean @default(false)
|
||||
bio String? @db.VarChar(160)
|
||||
bio_detail String? @db.VarChar(2000)
|
||||
must_reset_password Boolean @default(false)
|
||||
avatar_data Bytes?
|
||||
active_product_id String?
|
||||
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
||||
idea_code_counter Int @default(0)
|
||||
min_quota_pct Int @default(20)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
roles UserRole[]
|
||||
|
|
@ -175,6 +185,7 @@ model Product {
|
|||
claude_questions ClaudeQuestion[]
|
||||
claude_jobs ClaudeJob[]
|
||||
ideas Idea[]
|
||||
idea_products IdeaProduct[]
|
||||
|
||||
@@unique([user_id, name])
|
||||
@@unique([user_id, code])
|
||||
|
|
@ -322,6 +333,11 @@ model ClaudeJob {
|
|||
finished_at DateTime?
|
||||
pushed_at DateTime?
|
||||
verify_result VerifyResult?
|
||||
model_id String?
|
||||
input_tokens Int?
|
||||
output_tokens Int?
|
||||
cache_read_tokens Int?
|
||||
cache_write_tokens Int?
|
||||
plan_snapshot String?
|
||||
branch String?
|
||||
pr_url String?
|
||||
|
|
@ -339,15 +355,31 @@ model ClaudeJob {
|
|||
@@map("claude_jobs")
|
||||
}
|
||||
|
||||
model ModelPrice {
|
||||
id String @id @default(cuid())
|
||||
model_id String @unique
|
||||
input_price_per_1m Decimal @db.Decimal(12, 6)
|
||||
output_price_per_1m Decimal @db.Decimal(12, 6)
|
||||
cache_read_price_per_1m Decimal @db.Decimal(12, 6)
|
||||
cache_write_price_per_1m Decimal @db.Decimal(12, 6)
|
||||
currency String @default("USD")
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@map("model_prices")
|
||||
}
|
||||
|
||||
model ClaudeWorker {
|
||||
id String @id @default(cuid())
|
||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
user_id String
|
||||
token ApiToken @relation(fields: [token_id], references: [id], onDelete: Cascade)
|
||||
token_id String
|
||||
product_id String?
|
||||
started_at DateTime @default(now())
|
||||
last_seen_at DateTime @default(now())
|
||||
product_id String?
|
||||
started_at DateTime @default(now())
|
||||
last_seen_at DateTime @default(now())
|
||||
last_quota_pct Int?
|
||||
last_quota_check_at DateTime?
|
||||
|
||||
@@unique([token_id])
|
||||
@@index([user_id, last_seen_at])
|
||||
|
|
@ -403,9 +435,11 @@ model Idea {
|
|||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
questions ClaudeQuestion[]
|
||||
jobs ClaudeJob[]
|
||||
logs IdeaLog[]
|
||||
questions ClaudeQuestion[]
|
||||
jobs ClaudeJob[]
|
||||
logs IdeaLog[]
|
||||
user_questions UserQuestion[]
|
||||
secondary_products IdeaProduct[]
|
||||
|
||||
@@unique([user_id, code])
|
||||
@@index([user_id, archived, status])
|
||||
|
|
@ -413,6 +447,20 @@ model Idea {
|
|||
@@map("ideas")
|
||||
}
|
||||
|
||||
model IdeaProduct {
|
||||
id String @id @default(cuid())
|
||||
idea_id String
|
||||
product_id String
|
||||
created_at DateTime @default(now())
|
||||
|
||||
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([idea_id, product_id])
|
||||
@@index([product_id])
|
||||
@@map("idea_products")
|
||||
}
|
||||
|
||||
model IdeaLog {
|
||||
id String @id @default(cuid())
|
||||
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
|
|
@ -426,6 +474,23 @@ model IdeaLog {
|
|||
@@map("idea_logs")
|
||||
}
|
||||
|
||||
model UserQuestion {
|
||||
id String @id @default(cuid())
|
||||
idea_id String
|
||||
user_id String
|
||||
question String @db.Text
|
||||
answer String? @db.Text
|
||||
status UserQuestionStatus @default(pending)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([idea_id, status])
|
||||
@@index([user_id])
|
||||
@@map("user_questions")
|
||||
}
|
||||
|
||||
model LoginPairing {
|
||||
id String @id @default(cuid())
|
||||
secret_hash String
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import { registerGetIdeaContextTool } from './tools/get-idea-context.js'
|
|||
import { registerUpdateIdeaGrillMdTool } from './tools/update-idea-grill-md.js'
|
||||
import { registerUpdateIdeaPlanMdTool } from './tools/update-idea-plan-md.js'
|
||||
import { registerLogIdeaDecisionTool } from './tools/log-idea-decision.js'
|
||||
import { registerGetWorkerSettingsTool } from './tools/get-worker-settings.js'
|
||||
import { registerWorkerHeartbeatTool } from './tools/worker-heartbeat.js'
|
||||
import { registerImplementNextStoryPrompt } from './prompts/implement-next-story.js'
|
||||
import { getAuth } from './auth.js'
|
||||
import { registerWorker } from './presence/worker.js'
|
||||
|
|
@ -89,6 +91,9 @@ async function main() {
|
|||
registerUpdateIdeaGrillMdTool(server)
|
||||
registerUpdateIdeaPlanMdTool(server)
|
||||
registerLogIdeaDecisionTool(server)
|
||||
// M13: worker quota-gate tools
|
||||
registerGetWorkerSettingsTool(server)
|
||||
registerWorkerHeartbeatTool(server)
|
||||
registerImplementNextStoryPrompt(server)
|
||||
|
||||
// Presence bootstrap MUST run before server.connect — the stdio transport
|
||||
|
|
|
|||
33
src/tools/get-worker-settings.ts
Normal file
33
src/tools/get-worker-settings.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// MCP read-tool: lees de worker-instellingen van de geauthenticeerde user.
|
||||
//
|
||||
// Worker roept dit aan vóór elke wait_for_job iteratie zodat hij weet
|
||||
// wanneer hij stand-by moet (pre-flight quota-gate).
|
||||
//
|
||||
// Auth: api-token; user_id afgeleid uit token. Demo mag.
|
||||
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { prisma } from '../prisma.js'
|
||||
import { getAuth } from '../auth.js'
|
||||
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||
|
||||
export function registerGetWorkerSettingsTool(server: McpServer) {
|
||||
server.registerTool(
|
||||
'get_worker_settings',
|
||||
{
|
||||
title: 'Get worker settings',
|
||||
description:
|
||||
'Read the authenticated user\'s worker settings (min_quota_pct). Worker should call this each iteration before doing the pre-flight quota probe.',
|
||||
inputSchema: {},
|
||||
},
|
||||
async () =>
|
||||
withToolErrors(async () => {
|
||||
const auth = await getAuth()
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: auth.userId },
|
||||
select: { min_quota_pct: true },
|
||||
})
|
||||
if (!user) return toolError('User not found')
|
||||
return toolJson({ min_quota_pct: user.min_quota_pct })
|
||||
}),
|
||||
)
|
||||
}
|
||||
81
src/tools/worker-heartbeat.ts
Normal file
81
src/tools/worker-heartbeat.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// MCP write-tool: worker rapporteert quota-pct na pre-flight probe.
|
||||
//
|
||||
// Aanvulling op de bestaande achtergrond-heartbeat (die alleen last_seen_at
|
||||
// elke 5s tickt). Deze tool wordt expliciet aangeroepen door de worker
|
||||
// nadat scripts/worker-quota-probe.sh een quota-meting heeft gedaan.
|
||||
//
|
||||
// Updates ClaudeWorker.{last_quota_pct, last_quota_check_at, last_seen_at}
|
||||
// en emit een pg_notify-event op 'scrum4me_changes' zodat de UI de
|
||||
// stand-by-badge real-time kan tonen.
|
||||
//
|
||||
// Auth: api-token; demo mag niet (worker is geen demo-flow).
|
||||
|
||||
import { z } from 'zod'
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { Client } from 'pg'
|
||||
import { prisma } from '../prisma.js'
|
||||
import { requireWriteAccess } from '../auth.js'
|
||||
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||
|
||||
const inputSchema = z.object({
|
||||
last_quota_pct: z.number().int().min(0).max(100),
|
||||
last_quota_check_at: z.string().datetime().optional(),
|
||||
})
|
||||
|
||||
export function registerWorkerHeartbeatTool(server: McpServer) {
|
||||
server.registerTool(
|
||||
'worker_heartbeat',
|
||||
{
|
||||
title: 'Worker heartbeat with quota',
|
||||
description:
|
||||
'Report the worker\'s most recent rate-limit quota percentage to the server. Updates ClaudeWorker.last_quota_pct + last_quota_check_at. Emits a SSE event so the UI can show stand-by status. Forbidden for demo accounts.',
|
||||
inputSchema,
|
||||
},
|
||||
async ({ last_quota_pct, last_quota_check_at }) =>
|
||||
withToolErrors(async () => {
|
||||
const auth = await requireWriteAccess()
|
||||
const checkAt = last_quota_check_at ? new Date(last_quota_check_at) : new Date()
|
||||
|
||||
const result = await prisma.claudeWorker.updateMany({
|
||||
where: { token_id: auth.tokenId },
|
||||
data: {
|
||||
last_seen_at: new Date(),
|
||||
last_quota_pct,
|
||||
last_quota_check_at: checkAt,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.count === 0) {
|
||||
return toolError(
|
||||
'Worker record not found — call register_worker first or wait for the next heartbeat tick',
|
||||
)
|
||||
}
|
||||
|
||||
// pg_notify zodat NavBar realtime kan updaten. Failure is non-fatal:
|
||||
// de DB-write is al gebeurd, alleen de live-update mist dan.
|
||||
try {
|
||||
const pg = new Client({ connectionString: process.env.DATABASE_URL })
|
||||
await pg.connect()
|
||||
await pg.query('SELECT pg_notify($1, $2)', [
|
||||
'scrum4me_changes',
|
||||
JSON.stringify({
|
||||
type: 'worker_heartbeat',
|
||||
user_id: auth.userId,
|
||||
token_id: auth.tokenId,
|
||||
last_quota_pct,
|
||||
last_quota_check_at: checkAt.toISOString(),
|
||||
}),
|
||||
])
|
||||
await pg.end()
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
return toolJson({
|
||||
ok: true,
|
||||
last_quota_pct,
|
||||
last_quota_check_at: checkAt.toISOString(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
2
vendor/scrum4me
vendored
2
vendor/scrum4me
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 2893573004cf1df28ff5ad69752ddcf8b66ddb1e
|
||||
Subproject commit 555ed8fe89f0a3c9e52098fa0590ab8ba16e357a
|
||||
Loading…
Add table
Add a link
Reference in a new issue