update_task_status now delegates to updateTaskStatusWithStoryPromotion
and surfaces story_status_change ('promoted' | 'demoted' | null) in the
response so Claude Code can act on story completion without a separate
read call.
get_claude_context adds an OR-filter on tasks so stories where every
task is DONE are skipped — only surfaces stories that still have work to
do (no tasks, or at least one non-DONE task).
127 lines
3.7 KiB
TypeScript
127 lines
3.7 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: 'ACTIVE' },
|
|
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 openTodos = await prisma.todo.findMany({
|
|
where: {
|
|
user_id: auth.userId,
|
|
done: false,
|
|
archived: false,
|
|
OR: [{ product_id: product_id }, { product_id: null }],
|
|
},
|
|
orderBy: { created_at: 'asc' },
|
|
take: 50,
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
description: true,
|
|
created_at: true,
|
|
},
|
|
})
|
|
|
|
return toolJson({
|
|
product,
|
|
active_sprint: activeSprint,
|
|
next_story: nextStory,
|
|
open_todos: openTodos,
|
|
})
|
|
}),
|
|
)
|
|
}
|