Webapp had Prisma-schema migrated: SprintStatus.ACTIVE→OPEN,
SprintStatus.COMPLETED→CLOSED, plus new SprintStatus.ARCHIVED. Also new
TaskStatus.EXCLUDED. scrum4me-mcp Prisma client was 110 commits behind,
causing runtime errors when reading sprint.status from the live DB:
Value 'OPEN' not found in enum 'SprintStatus'
Symptom: TASK_IMPLEMENTATION jobs in QUEUED status were claimed by
tryClaimJob (raw SQL succeeds), then getFullJobContext crashed on the
findUnique with the enum error → rollbackClaim → loop forever until
UNHEALTHY (5 consecutive failures).
Fix:
- Updated vendor/scrum4me submodule to current main (3c77342).
- Re-ran sync-schema.sh → prisma/schema.prisma now has
SprintStatus { OPEN, CLOSED, ARCHIVED, FAILED } and
TaskStatus including EXCLUDED.
- src/lib/tasks-status-update.ts: ACTIVE→OPEN, COMPLETED→CLOSED.
- src/status.ts: TASK_DB_TO_API + TASK_API_TO_DB krijgen EXCLUDED entry.
- src/tools/get-claude-context.ts: status: 'ACTIVE' → status: 'OPEN'.
Tests: 340 passed (38 files). Typecheck OK.
Na merge + docker rebuild met cache-bust pakt de runner sprint-tasks weer
op zonder enum-error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
3.8 KiB
TypeScript
129 lines
3.8 KiB
TypeScript
import { z } from 'zod'
|
|
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'
|
|
import { storyStatusToApi, taskStatusToApi } from '../status.js'
|
|
|
|
const inputSchema = z.object({
|
|
product_id: z.string().min(1),
|
|
})
|
|
|
|
export function registerGetClaudeContextTool(server: McpServer) {
|
|
server.registerTool(
|
|
'get_claude_context',
|
|
{
|
|
title: 'Bundled context for Claude Code',
|
|
description:
|
|
'Fetch product, active sprint, next story (with tasks) and open todos in one call. ' +
|
|
'Always start a Scrum4Me workflow with this tool.',
|
|
inputSchema,
|
|
annotations: { readOnlyHint: true },
|
|
},
|
|
async ({ product_id }) =>
|
|
withToolErrors(async () => {
|
|
const auth = await getAuth()
|
|
|
|
const product = await prisma.product.findFirst({
|
|
where: {
|
|
id: product_id,
|
|
OR: [
|
|
{ user_id: auth.userId },
|
|
{ members: { some: { user_id: auth.userId } } },
|
|
],
|
|
},
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
name: true,
|
|
description: true,
|
|
repo_url: true,
|
|
definition_of_done: true,
|
|
},
|
|
})
|
|
|
|
if (!product) {
|
|
return toolError(`Product ${product_id} not found or not accessible`)
|
|
}
|
|
|
|
const activeSprint = await prisma.sprint.findFirst({
|
|
where: { product_id, status: 'OPEN' },
|
|
orderBy: { created_at: 'desc' },
|
|
select: { id: true, sprint_goal: true, status: true },
|
|
})
|
|
|
|
let nextStory = null
|
|
if (activeSprint) {
|
|
const story = await prisma.story.findFirst({
|
|
where: {
|
|
sprint_id: activeSprint.id,
|
|
status: { in: ['OPEN', 'IN_SPRINT'] },
|
|
OR: [
|
|
{ tasks: { none: {} } },
|
|
{ tasks: { some: { status: { not: 'DONE' } } } },
|
|
],
|
|
},
|
|
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
title: true,
|
|
description: true,
|
|
acceptance_criteria: true,
|
|
priority: true,
|
|
status: true,
|
|
tasks: {
|
|
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
description: true,
|
|
implementation_plan: true,
|
|
priority: true,
|
|
sort_order: true,
|
|
status: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if (story) {
|
|
nextStory = {
|
|
...story,
|
|
status: storyStatusToApi(story.status),
|
|
tasks: story.tasks.map((t, i) => ({
|
|
...t,
|
|
code: story.code ? `${story.code}.${i + 1}` : null,
|
|
status: taskStatusToApi(t.status),
|
|
})),
|
|
}
|
|
}
|
|
}
|
|
|
|
const openIdeas = await prisma.idea.findMany({
|
|
where: {
|
|
user_id: auth.userId,
|
|
archived: false,
|
|
status: { not: 'PLANNED' },
|
|
OR: [{ product_id: product_id }, { product_id: null }],
|
|
},
|
|
orderBy: { created_at: 'asc' },
|
|
take: 50,
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
title: true,
|
|
description: true,
|
|
status: true,
|
|
created_at: true,
|
|
},
|
|
})
|
|
|
|
return toolJson({
|
|
product,
|
|
active_sprint: activeSprint,
|
|
next_story: nextStory,
|
|
open_ideas: openIdeas,
|
|
})
|
|
}),
|
|
)
|
|
}
|