Merge pull request #2 from madhura68/feat/mcp-authoring-tools
feat: add 3 authoring tools — create_pbi / create_story / create_task
This commit is contained in:
commit
68a286da70
6 changed files with 237 additions and 1 deletions
|
|
@ -18,6 +18,9 @@ activity and create todos via native tool calls instead of curl.
|
||||||
| `log_test_result` | Append TEST_RESULT (PASSED/FAILED) | no |
|
| `log_test_result` | Append TEST_RESULT (PASSED/FAILED) | no |
|
||||||
| `log_commit` | Append COMMIT with hash and message | no |
|
| `log_commit` | Append COMMIT with hash and message | no |
|
||||||
| `create_todo` | Add a todo, optionally scoped to a product | no |
|
| `create_todo` | Add a todo, optionally scoped to a product | no |
|
||||||
|
| `create_pbi` | Add a Product Backlog Item to a product (auto sort_order) | no |
|
||||||
|
| `create_story` | Add a story under a PBI (status=OPEN, lands in product backlog) | no |
|
||||||
|
| `create_task` | Add a task under a story (status=TO_DO, inherits sprint_id) | no |
|
||||||
| `ask_user_question` | Post a question to the active user about a story; optional `wait_seconds` (max 600) polls for the answer | no |
|
| `ask_user_question` | Post a question to the active user about a story; optional `wait_seconds` (max 600) polls for the answer | no |
|
||||||
| `get_question_answer` | Fetch the current status + answer of a previously-asked question | n/a |
|
| `get_question_answer` | Fetch the current status + answer of a previously-asked question | n/a |
|
||||||
| `list_open_questions` | List own open/answered questions, most recent first (max 50) | n/a |
|
| `list_open_questions` | List own open/answered questions, most recent first (max 50) | n/a |
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ async function main() {
|
||||||
const tools = await client.listTools()
|
const tools = await client.listTools()
|
||||||
log(
|
log(
|
||||||
'tools/list',
|
'tools/list',
|
||||||
tools.tools.length === 13,
|
tools.tools.length === 16,
|
||||||
`${tools.tools.length} tools: ${tools.tools.map((t) => t.name).join(', ')}`,
|
`${tools.tools.length} tools: ${tools.tools.map((t) => t.name).join(', ')}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import { registerLogImplementationTool } from './tools/log-implementation.js'
|
||||||
import { registerLogTestResultTool } from './tools/log-test-result.js'
|
import { registerLogTestResultTool } from './tools/log-test-result.js'
|
||||||
import { registerLogCommitTool } from './tools/log-commit.js'
|
import { registerLogCommitTool } from './tools/log-commit.js'
|
||||||
import { registerCreateTodoTool } from './tools/create-todo.js'
|
import { registerCreateTodoTool } from './tools/create-todo.js'
|
||||||
|
import { registerCreatePbiTool } from './tools/create-pbi.js'
|
||||||
|
import { registerCreateStoryTool } from './tools/create-story.js'
|
||||||
|
import { registerCreateTaskTool } from './tools/create-task.js'
|
||||||
import { registerAskUserQuestionTool } from './tools/ask-user-question.js'
|
import { registerAskUserQuestionTool } from './tools/ask-user-question.js'
|
||||||
import { registerGetQuestionAnswerTool } from './tools/get-question-answer.js'
|
import { registerGetQuestionAnswerTool } from './tools/get-question-answer.js'
|
||||||
import { registerListOpenQuestionsTool } from './tools/list-open-questions.js'
|
import { registerListOpenQuestionsTool } from './tools/list-open-questions.js'
|
||||||
|
|
@ -37,6 +40,9 @@ async function main() {
|
||||||
registerLogTestResultTool(server)
|
registerLogTestResultTool(server)
|
||||||
registerLogCommitTool(server)
|
registerLogCommitTool(server)
|
||||||
registerCreateTodoTool(server)
|
registerCreateTodoTool(server)
|
||||||
|
registerCreatePbiTool(server)
|
||||||
|
registerCreateStoryTool(server)
|
||||||
|
registerCreateTaskTool(server)
|
||||||
registerAskUserQuestionTool(server)
|
registerAskUserQuestionTool(server)
|
||||||
registerGetQuestionAnswerTool(server)
|
registerGetQuestionAnswerTool(server)
|
||||||
registerListOpenQuestionsTool(server)
|
registerListOpenQuestionsTool(server)
|
||||||
|
|
|
||||||
68
src/tools/create-pbi.ts
Normal file
68
src/tools/create-pbi.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// MCP authoring tool: create een Product Backlog Item.
|
||||||
|
//
|
||||||
|
// Sort_order wordt automatisch op last+1 binnen de prioriteits-groep gezet als
|
||||||
|
// niet meegegeven. Code-veld blijft null — auto-codes (PBI-1, PBI-2, …) worden
|
||||||
|
// door de Scrum4Me-app gegenereerd, kan optioneel later via UI worden gezet.
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
|
import { prisma } from '../prisma.js'
|
||||||
|
import { requireWriteAccess } from '../auth.js'
|
||||||
|
import { userCanAccessProduct } from '../access.js'
|
||||||
|
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
product_id: z.string().min(1),
|
||||||
|
title: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(4000).optional(),
|
||||||
|
priority: z.number().int().min(1).max(4),
|
||||||
|
sort_order: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerCreatePbiTool(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
'create_pbi',
|
||||||
|
{
|
||||||
|
title: 'Create PBI',
|
||||||
|
description:
|
||||||
|
'Add a Product Backlog Item to a product. Sort_order auto-set to last+1 within the priority group if not provided. Forbidden for demo accounts.',
|
||||||
|
inputSchema,
|
||||||
|
},
|
||||||
|
async ({ product_id, title, description, priority, sort_order }) =>
|
||||||
|
withToolErrors(async () => {
|
||||||
|
const auth = await requireWriteAccess()
|
||||||
|
if (!(await userCanAccessProduct(product_id, auth.userId))) {
|
||||||
|
return toolError(`Product ${product_id} not found or not accessible`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedSortOrder = sort_order
|
||||||
|
if (resolvedSortOrder === undefined) {
|
||||||
|
const last = await prisma.pbi.findFirst({
|
||||||
|
where: { product_id, priority },
|
||||||
|
orderBy: { sort_order: 'desc' },
|
||||||
|
select: { sort_order: true },
|
||||||
|
})
|
||||||
|
resolvedSortOrder = (last?.sort_order ?? 0) + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const pbi = await prisma.pbi.create({
|
||||||
|
data: {
|
||||||
|
product_id,
|
||||||
|
title,
|
||||||
|
description: description ?? null,
|
||||||
|
priority,
|
||||||
|
sort_order: resolvedSortOrder,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
priority: true,
|
||||||
|
sort_order: true,
|
||||||
|
created_at: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return toolJson(pbi)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
80
src/tools/create-story.ts
Normal file
80
src/tools/create-story.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// MCP authoring tool: create een Story onder een bestaande PBI.
|
||||||
|
//
|
||||||
|
// product_id wordt afgeleid uit de PBI (denormalized FK conform CLAUDE.md
|
||||||
|
// convention — nooit vertrouwen op client-input). status='OPEN' default;
|
||||||
|
// landt in de Product Backlog, niet auto in een sprint.
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
|
import { prisma } from '../prisma.js'
|
||||||
|
import { requireWriteAccess } from '../auth.js'
|
||||||
|
import { userCanAccessProduct } from '../access.js'
|
||||||
|
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
pbi_id: z.string().min(1),
|
||||||
|
title: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(4000).optional(),
|
||||||
|
acceptance_criteria: z.string().max(4000).optional(),
|
||||||
|
priority: z.number().int().min(1).max(4),
|
||||||
|
sort_order: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerCreateStoryTool(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
'create_story',
|
||||||
|
{
|
||||||
|
title: 'Create story',
|
||||||
|
description:
|
||||||
|
'Add a story under an existing PBI. Status defaults to OPEN (lands in product backlog, not in a sprint). Sort_order auto-set to last+1 within the PBI/priority group if not provided. Forbidden for demo accounts.',
|
||||||
|
inputSchema,
|
||||||
|
},
|
||||||
|
async ({ pbi_id, title, description, acceptance_criteria, priority, sort_order }) =>
|
||||||
|
withToolErrors(async () => {
|
||||||
|
const auth = await requireWriteAccess()
|
||||||
|
|
||||||
|
const pbi = await prisma.pbi.findUnique({
|
||||||
|
where: { id: pbi_id },
|
||||||
|
select: { product_id: true },
|
||||||
|
})
|
||||||
|
if (!pbi) return toolError(`PBI ${pbi_id} not found`)
|
||||||
|
if (!(await userCanAccessProduct(pbi.product_id, auth.userId))) {
|
||||||
|
return toolError(`PBI ${pbi_id} not accessible`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedSortOrder = sort_order
|
||||||
|
if (resolvedSortOrder === undefined) {
|
||||||
|
const last = await prisma.story.findFirst({
|
||||||
|
where: { pbi_id, priority },
|
||||||
|
orderBy: { sort_order: 'desc' },
|
||||||
|
select: { sort_order: true },
|
||||||
|
})
|
||||||
|
resolvedSortOrder = (last?.sort_order ?? 0) + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const story = await prisma.story.create({
|
||||||
|
data: {
|
||||||
|
pbi_id,
|
||||||
|
product_id: pbi.product_id, // denormalized uit DB-parent, niet uit input
|
||||||
|
title,
|
||||||
|
description: description ?? null,
|
||||||
|
acceptance_criteria: acceptance_criteria ?? null,
|
||||||
|
priority,
|
||||||
|
sort_order: resolvedSortOrder,
|
||||||
|
status: 'OPEN',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
acceptance_criteria: true,
|
||||||
|
priority: true,
|
||||||
|
sort_order: true,
|
||||||
|
status: true,
|
||||||
|
created_at: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return toolJson(story)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
79
src/tools/create-task.ts
Normal file
79
src/tools/create-task.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// MCP authoring tool: create een Task onder een bestaande Story.
|
||||||
|
//
|
||||||
|
// sprint_id wordt afgeleid uit de Story (denormalized FK). Als de story in
|
||||||
|
// een sprint zit, erft de task die sprint_id; anders null. Status='TO_DO'.
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
|
import { prisma } from '../prisma.js'
|
||||||
|
import { requireWriteAccess } from '../auth.js'
|
||||||
|
import { userCanAccessProduct } from '../access.js'
|
||||||
|
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
story_id: z.string().min(1),
|
||||||
|
title: z.string().min(1).max(200),
|
||||||
|
description: z.string().max(4000).optional(),
|
||||||
|
implementation_plan: z.string().max(8000).optional(),
|
||||||
|
priority: z.number().int().min(1).max(4),
|
||||||
|
sort_order: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerCreateTaskTool(server: McpServer) {
|
||||||
|
server.registerTool(
|
||||||
|
'create_task',
|
||||||
|
{
|
||||||
|
title: 'Create task',
|
||||||
|
description:
|
||||||
|
'Add a task under an existing story. Inherits sprint_id from the story (denormalized). Status defaults to TO_DO. Sort_order auto-set to last+1 within the story/priority group if not provided. Forbidden for demo accounts.',
|
||||||
|
inputSchema,
|
||||||
|
},
|
||||||
|
async ({ story_id, title, description, implementation_plan, priority, sort_order }) =>
|
||||||
|
withToolErrors(async () => {
|
||||||
|
const auth = await requireWriteAccess()
|
||||||
|
|
||||||
|
const story = await prisma.story.findUnique({
|
||||||
|
where: { id: story_id },
|
||||||
|
select: { product_id: true, sprint_id: true },
|
||||||
|
})
|
||||||
|
if (!story) return toolError(`Story ${story_id} not found`)
|
||||||
|
if (!(await userCanAccessProduct(story.product_id, auth.userId))) {
|
||||||
|
return toolError(`Story ${story_id} not accessible`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedSortOrder = sort_order
|
||||||
|
if (resolvedSortOrder === undefined) {
|
||||||
|
const last = await prisma.task.findFirst({
|
||||||
|
where: { story_id, priority },
|
||||||
|
orderBy: { sort_order: 'desc' },
|
||||||
|
select: { sort_order: true },
|
||||||
|
})
|
||||||
|
resolvedSortOrder = (last?.sort_order ?? 0) + 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await prisma.task.create({
|
||||||
|
data: {
|
||||||
|
story_id,
|
||||||
|
sprint_id: story.sprint_id, // denormalized — erf van story
|
||||||
|
title,
|
||||||
|
description: description ?? null,
|
||||||
|
implementation_plan: implementation_plan ?? null,
|
||||||
|
priority,
|
||||||
|
sort_order: resolvedSortOrder,
|
||||||
|
status: 'TO_DO',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
implementation_plan: true,
|
||||||
|
priority: true,
|
||||||
|
sort_order: true,
|
||||||
|
status: true,
|
||||||
|
created_at: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return toolJson(task)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue