From f5a630c143fedc7c413c47b44e75bf3f1023f9b1 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 23:02:05 +0200 Subject: [PATCH] feat(ST-704): status mappers and shared error helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/errors.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/status.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/errors.ts create mode 100644 src/status.ts diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..e7298cc --- /dev/null +++ b/src/errors.ts @@ -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, + } +} + +export async function withToolErrors( + fn: () => Promise, +): Promise { + 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)) + } +} diff --git a/src/status.ts b/src/status.ts new file mode 100644 index 0000000..74e2e52 --- /dev/null +++ b/src/status.ts @@ -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 + +const TASK_API_TO_DB: Record = { + 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 + +const STORY_API_TO_DB: Record = { + 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)