From d50075d960966cb94fa24f315941604b41d4e19e Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Wed, 6 May 2026 04:23:31 +0200 Subject: [PATCH] feat(M13): get_worker_settings + worker_heartbeat tools (v0.7.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- package.json | 2 +- prisma/schema.prisma | 77 +++++++++++++++++++++++++++--- src/index.ts | 5 ++ src/tools/get-worker-settings.ts | 33 +++++++++++++ src/tools/worker-heartbeat.ts | 81 ++++++++++++++++++++++++++++++++ vendor/scrum4me | 2 +- 6 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/tools/get-worker-settings.ts create mode 100644 src/tools/worker-heartbeat.ts diff --git a/package.json b/package.json index c34156c..913b21c 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f15b47c..b286071 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/index.ts b/src/index.ts index 0585907..a6d6c72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/tools/get-worker-settings.ts b/src/tools/get-worker-settings.ts new file mode 100644 index 0000000..9b2a82b --- /dev/null +++ b/src/tools/get-worker-settings.ts @@ -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 }) + }), + ) +} diff --git a/src/tools/worker-heartbeat.ts b/src/tools/worker-heartbeat.ts new file mode 100644 index 0000000..ed1023d --- /dev/null +++ b/src/tools/worker-heartbeat.ts @@ -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(), + }) + }), + ) +} diff --git a/vendor/scrum4me b/vendor/scrum4me index 2893573..555ed8f 160000 --- a/vendor/scrum4me +++ b/vendor/scrum4me @@ -1 +1 @@ -Subproject commit 2893573004cf1df28ff5ad69752ddcf8b66ddb1e +Subproject commit 555ed8fe89f0a3c9e52098fa0590ab8ba16e357a