feat(ST-707): log tools — implementation, test_result, commit

Three write tools that append StoryLog entries:
- log_implementation: type=IMPLEMENTATION_PLAN
- log_test_result: type=TEST_RESULT, status PASSED|FAILED
- log_commit: type=COMMIT with hash and message

All accept optional metadata in input but skip writing it for now —
the StoryLog.metadata JSONB column lands with Scrum4Me PR #2.
After that PR merges, run sync-schema and add `metadata` to each
prisma.create's data field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 23:06:50 +02:00
parent e3f9476568
commit c39b337fcc
4 changed files with 138 additions and 1 deletions

View file

@ -6,6 +6,9 @@ import { registerListProductsTool } from './tools/list-products.js'
import { registerGetClaudeContextTool } from './tools/get-claude-context.js' import { registerGetClaudeContextTool } from './tools/get-claude-context.js'
import { registerUpdateTaskStatusTool } from './tools/update-task-status.js' import { registerUpdateTaskStatusTool } from './tools/update-task-status.js'
import { registerUpdateTaskPlanTool } from './tools/update-task-plan.js' import { registerUpdateTaskPlanTool } from './tools/update-task-plan.js'
import { registerLogImplementationTool } from './tools/log-implementation.js'
import { registerLogTestResultTool } from './tools/log-test-result.js'
import { registerLogCommitTool } from './tools/log-commit.js'
const VERSION = '0.1.0' const VERSION = '0.1.0'
@ -24,7 +27,10 @@ async function main() {
registerGetClaudeContextTool(server) registerGetClaudeContextTool(server)
registerUpdateTaskStatusTool(server) registerUpdateTaskStatusTool(server)
registerUpdateTaskPlanTool(server) registerUpdateTaskPlanTool(server)
// Log tools, create_todo and prompts in ST-707..ST-709. registerLogImplementationTool(server)
registerLogTestResultTool(server)
registerLogCommitTool(server)
// create_todo and prompts in ST-708..ST-709.
const transport = new StdioServerTransport() const transport = new StdioServerTransport()
await server.connect(transport) await server.connect(transport)

45
src/tools/log-commit.ts Normal file
View file

@ -0,0 +1,45 @@
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js'
import { userCanAccessStory } from '../access.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
const inputSchema = z.object({
story_id: z.string().min(1),
content: z.string().min(1),
commit_hash: z.string().min(1),
commit_message: z.string().min(1),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export function registerLogCommitTool(server: McpServer) {
server.registerTool(
'log_commit',
{
title: 'Log commit',
description:
'Append a COMMIT entry to a story log with hash and message. ' +
'Forbidden for demo accounts.',
inputSchema,
},
async ({ story_id, content, commit_hash, commit_message }) =>
withToolErrors(async () => {
const auth = await requireWriteAccess()
if (!(await userCanAccessStory(story_id, auth.userId))) {
return toolError(`Story ${story_id} not found or not accessible`)
}
const log = await prisma.storyLog.create({
data: {
story_id,
type: 'COMMIT',
content,
commit_hash,
commit_message,
},
select: { id: true, created_at: true },
})
return toolJson({ id: log.id, created_at: log.created_at })
}),
)
}

View file

@ -0,0 +1,43 @@
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js'
import { userCanAccessStory } from '../access.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
// metadata is accepted on input but not yet written — schema sync after
// Scrum4Me PR #2 lands adds the StoryLog.metadata JSONB column.
const inputSchema = z.object({
story_id: z.string().min(1),
content: z.string().min(1),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export function registerLogImplementationTool(server: McpServer) {
server.registerTool(
'log_implementation',
{
title: 'Log implementation plan',
description:
'Append an IMPLEMENTATION_PLAN entry to a story log. ' +
'Forbidden for demo accounts.',
inputSchema,
},
async ({ story_id, content }) =>
withToolErrors(async () => {
const auth = await requireWriteAccess()
if (!(await userCanAccessStory(story_id, auth.userId))) {
return toolError(`Story ${story_id} not found or not accessible`)
}
const log = await prisma.storyLog.create({
data: {
story_id,
type: 'IMPLEMENTATION_PLAN',
content,
},
select: { id: true, created_at: true },
})
return toolJson({ id: log.id, created_at: log.created_at })
}),
)
}

View file

@ -0,0 +1,43 @@
import { z } from 'zod'
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { prisma } from '../prisma.js'
import { requireWriteAccess } from '../auth.js'
import { userCanAccessStory } from '../access.js'
import { toolError, toolJson, withToolErrors } from '../errors.js'
const inputSchema = z.object({
story_id: z.string().min(1),
content: z.string().min(1),
status: z.enum(['PASSED', 'FAILED']),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export function registerLogTestResultTool(server: McpServer) {
server.registerTool(
'log_test_result',
{
title: 'Log test result',
description:
'Append a TEST_RESULT entry (PASSED or FAILED) to a story log. ' +
'Forbidden for demo accounts.',
inputSchema,
},
async ({ story_id, content, status }) =>
withToolErrors(async () => {
const auth = await requireWriteAccess()
if (!(await userCanAccessStory(story_id, auth.userId))) {
return toolError(`Story ${story_id} not found or not accessible`)
}
const log = await prisma.storyLog.create({
data: {
story_id,
type: 'TEST_RESULT',
content,
status,
},
select: { id: true, created_at: true },
})
return toolJson({ id: log.id, created_at: log.created_at })
}),
)
}