feat(ST-704): status mappers and shared error helpers
- src/status.ts: bidirectional Task/Story status mappers — DB stays UPPER_SNAKE, MCP tools expose lowercase (matches REST API contract) - src/errors.ts: formatZodError, toolError, toolJson and the withToolErrors() wrapper so each tool turns thrown exceptions (PermissionDenied, ZodError, generic) into structured MCP errors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b52b1cedd
commit
f5a630c143
2 changed files with 94 additions and 0 deletions
45
src/errors.ts
Normal file
45
src/errors.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { z } from 'zod'
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { PermissionDeniedError } from './auth.js'
|
||||
|
||||
export function formatZodError(err: z.ZodError): string {
|
||||
return err.issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.length ? issue.path.join('.') : '(root)'
|
||||
return `${path}: ${issue.message}`
|
||||
})
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
export function toolError(message: string): CallToolResult {
|
||||
return {
|
||||
content: [{ type: 'text', text: message }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function toolJson(value: unknown): CallToolResult {
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
structuredContent: value as Record<string, unknown>,
|
||||
}
|
||||
}
|
||||
|
||||
export async function withToolErrors<T>(
|
||||
fn: () => Promise<CallToolResult>,
|
||||
): Promise<CallToolResult> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
if (err instanceof PermissionDeniedError) {
|
||||
return toolError(`PERMISSION_DENIED: ${err.message}`)
|
||||
}
|
||||
if (err instanceof z.ZodError) {
|
||||
return toolError(`VALIDATION_ERROR: ${formatZodError(err)}`)
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return toolError(err.message)
|
||||
}
|
||||
return toolError(String(err))
|
||||
}
|
||||
}
|
||||
49
src/status.ts
Normal file
49
src/status.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { TaskStatus, StoryStatus } from '@prisma/client'
|
||||
|
||||
const TASK_DB_TO_API = {
|
||||
TO_DO: 'todo',
|
||||
IN_PROGRESS: 'in_progress',
|
||||
REVIEW: 'review',
|
||||
DONE: 'done',
|
||||
} as const satisfies Record<TaskStatus, string>
|
||||
|
||||
const TASK_API_TO_DB: Record<string, TaskStatus> = {
|
||||
todo: 'TO_DO',
|
||||
in_progress: 'IN_PROGRESS',
|
||||
review: 'REVIEW',
|
||||
done: 'DONE',
|
||||
}
|
||||
|
||||
const STORY_DB_TO_API = {
|
||||
OPEN: 'open',
|
||||
IN_SPRINT: 'in_sprint',
|
||||
DONE: 'done',
|
||||
} as const satisfies Record<StoryStatus, string>
|
||||
|
||||
const STORY_API_TO_DB: Record<string, StoryStatus> = {
|
||||
open: 'OPEN',
|
||||
in_sprint: 'IN_SPRINT',
|
||||
done: 'DONE',
|
||||
}
|
||||
|
||||
export type TaskStatusApi = (typeof TASK_DB_TO_API)[TaskStatus]
|
||||
export type StoryStatusApi = (typeof STORY_DB_TO_API)[StoryStatus]
|
||||
|
||||
export function taskStatusToApi(s: TaskStatus): TaskStatusApi {
|
||||
return TASK_DB_TO_API[s]
|
||||
}
|
||||
|
||||
export function taskStatusFromApi(s: string): TaskStatus | null {
|
||||
return TASK_API_TO_DB[s.toLowerCase()] ?? null
|
||||
}
|
||||
|
||||
export function storyStatusToApi(s: StoryStatus): StoryStatusApi {
|
||||
return STORY_DB_TO_API[s]
|
||||
}
|
||||
|
||||
export function storyStatusFromApi(s: string): StoryStatus | null {
|
||||
return STORY_API_TO_DB[s.toLowerCase()] ?? null
|
||||
}
|
||||
|
||||
export const TASK_STATUS_API_VALUES = Object.values(TASK_DB_TO_API)
|
||||
export const STORY_STATUS_API_VALUES = Object.values(STORY_DB_TO_API)
|
||||
Loading…
Add table
Add a link
Reference in a new issue