scrum4me-mcp/scripts/smoke-test.ts
Madhura68 7b955d31ac feat(ST-1102): add 4 question-channel MCP tools (M11)
Vier nieuwe tools voor het Claude vraag-antwoord-kanaal:
- ask_user_question (write): post een gestructureerde vraag aan de actieve
  Scrum4Me-gebruiker over een story; default async (returnt direct met
  question_id + status='open'); optionele wait_seconds (max 600) polt elke 2s
  tot het antwoord er is of timeout — daarna status='pending' zodat Claude met
  get_question_answer later kan ophalen
- get_question_answer (read): huidige status + antwoord van een eerder
  gestelde vraag
- list_open_questions (read): eigen vragen met status open/answered, max 50,
  meest recente eerst
- cancel_question (write, asker-only): atomic UPDATE WHERE asked_by + status=
  'open' zodat alleen eigen open vragen geannuleerd worden

Allemaal achter access-check via userCanAccessStory/Product en demo-blok via
requireWriteAccess (volgt patroon van create-todo en bestaande log-tools).

Submodule vendor/scrum4me bumpt naar Scrum4Me commit 79367dd (M11 ST-1101) —
bevat het ClaudeQuestion-model en notify_question_change-trigger waar deze
tools tegen werken.

scripts/smoke-test.ts: 13 tools verwacht (was 9); list_open_questions
toegevoegd als read-tool-coverage. Build + tools/list groen — verdere e2e via
MCP Inspector na PR-merge omdat de seed een nieuwe API-token heeft
gegenereerd en .env een nieuwe waarde nodig heeft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:00:59 +02:00

105 lines
3.6 KiB
TypeScript

// One-shot e2e smoke test against the live Scrum4Me database.
// Reads .env, spawns the built server over stdio, calls each tool.
//
// npm run build && npx tsx scripts/smoke-test.ts
//
// Exits 0 on success, 1 on any tool failure.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { readFileSync } from 'fs'
import { resolve } from 'path'
function loadEnv(): Record<string, string> {
const file = readFileSync(resolve(process.cwd(), '.env'), 'utf8')
const out: Record<string, string> = {}
for (const line of file.split('\n')) {
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/)
if (m) out[m[1]] = m[2].replace(/^"(.*)"$/, '$1')
}
return out
}
async function callTool(client: Client, name: string, args: Record<string, unknown> = {}) {
const res = await client.callTool({ name, arguments: args })
const text = (res.content as Array<{ type: string; text?: string }>)
.filter((c) => c.type === 'text')
.map((c) => c.text)
.join('\n')
return { isError: Boolean(res.isError), text }
}
async function main() {
const env = { ...process.env, ...loadEnv() } as Record<string, string>
if (!env.DATABASE_URL) throw new Error('DATABASE_URL missing')
if (!env.SCRUM4ME_TOKEN) throw new Error('SCRUM4ME_TOKEN missing')
const transport = new StdioClientTransport({
command: 'node',
args: ['dist/index.js'],
env,
})
const client = new Client({ name: 'smoke-test', version: '0.0.1' })
await client.connect(transport)
let failed = 0
const log = (label: string, ok: boolean, detail: string) => {
const tag = ok ? 'PASS' : 'FAIL'
console.log(`[${tag}] ${label}: ${detail.slice(0, 240)}${detail.length > 240 ? '…' : ''}`)
if (!ok) failed++
}
// tools/list
const tools = await client.listTools()
log(
'tools/list',
tools.tools.length === 13,
`${tools.tools.length} tools: ${tools.tools.map((t) => t.name).join(', ')}`,
)
// health
const health = await callTool(client, 'health')
log('health', !health.isError, health.text)
const healthJson = JSON.parse(health.text)
log('health.database', healthJson.database === 'ok', `database=${healthJson.database}`)
// list_products
const products = await callTool(client, 'list_products')
log('list_products', !products.isError, products.text)
const productList = JSON.parse(products.text) as Array<{ id: string; name: string; code: string | null }>
log('list_products.count', productList.length > 0, `count=${productList.length}`)
if (productList.length > 0) {
const productId = productList[0].id
const ctx = await callTool(client, 'get_claude_context', { product_id: productId })
log('get_claude_context', !ctx.isError, ctx.text)
}
// list_open_questions (M11 — read-only, geen write nodig voor smoke-test)
const openQs = await callTool(client, 'list_open_questions')
log('list_open_questions', !openQs.isError, openQs.text)
if (!openQs.isError) {
const parsed = JSON.parse(openQs.text) as { count: number }
log('list_open_questions.shape', typeof parsed.count === 'number', `count=${parsed.count}`)
}
// prompts/list
const prompts = await client.listPrompts()
log(
'prompts/list',
prompts.prompts.some((p) => p.name === 'implement_next_story'),
`prompts: ${prompts.prompts.map((p) => p.name).join(', ')}`,
)
await client.close()
if (failed > 0) {
console.error(`\n${failed} check(s) failed`)
process.exit(1)
}
console.log('\nall checks passed')
}
main().catch((err) => {
console.error('fatal:', err)
process.exit(1)
})