import { randomUUID } from 'node:crypto'; import { PRODUCT_ID } from '../lib/product-config.js'; import { createNote, getNote, listNotes } from '../modules/notes/repository.js'; import type { NoteDoc } from '../modules/notes/types.js'; import { createNoteAgentAction } from '../modules/note-agent-actions/repository.js'; import type { NoteAgentActionDoc } from '../modules/note-agent-actions/types.js'; import { CreateNoteDraftToolOutputSchema, GetNoteToolOutputSchema, ListNotesToolOutputSchema, NOTES_MCP_TOOL_NAMES, NotesMcpToolDefinitions, SearchNotesToolOutputSchema, type CreateNoteDraftToolInput, type GetNoteToolInput, type ListNotesToolInput, type NoteToolRole, type SearchNotesToolInput, } from './note-tool-contracts.js'; export interface NotesMcpLogger { info(obj: Record, msg?: string): void; warn(obj: Record, msg?: string): void; error(obj: Record, msg?: string): void; debug(obj: Record, msg?: string): void; } export interface NotesMcpRequest { id: string; headers: { authorization?: string | undefined }; jwtPayload?: { sub?: string; role?: string; productId?: string }; log: NotesMcpLogger; } export interface NotesMcpTool { name: string; description: string; requiredRole: NoteToolRole; readOnly: boolean; inputSchema: { parse(input: unknown): TInput }; execute(args: TInput, req: NotesMcpRequest): Promise; } function requireUserId(req: NotesMcpRequest): string { const userId = req.jwtPayload?.sub; if (!userId) { throw new Error('Authenticated user is required'); } return userId; } function mapNoteSummary(note: NoteDoc) { return { id: note.id, workspaceId: note.workspaceId, title: note.title, status: note.status, updatedAt: note.updatedAt, tags: note.tags, }; } function mapNote(note: NoteDoc) { return GetNoteToolOutputSchema.parse({ id: note.id, workspaceId: note.workspaceId, title: note.title, body: note.body, status: note.status, tags: note.tags, links: note.links, sourceType: note.sourceType, sourceUri: note.sourceUri, agentId: note.agentId, createdAt: note.createdAt, updatedAt: note.updatedAt, }); } async function executeListNotes(args: ListNotesToolInput, req: NotesMcpRequest) { const userId = requireUserId(req); const result = await listNotes(userId, PRODUCT_ID, args); return ListNotesToolOutputSchema.parse({ items: result.items.map(mapNoteSummary), total: result.total, limit: args.limit, offset: args.offset, }); } async function executeGetNote(args: GetNoteToolInput, req: NotesMcpRequest) { const userId = requireUserId(req); const note = await getNote(args.noteId, args.workspaceId); if (!note || note.userId !== userId || note.productId !== PRODUCT_ID) { throw new Error('Note not found'); } return mapNote(note); } function getMatchFields(note: NoteDoc, query: string): Array<'title' | 'body' | 'tags'> { const lower = query.toLowerCase(); const fields: Array<'title' | 'body' | 'tags'> = []; if (note.title.toLowerCase().includes(lower)) { fields.push('title'); } if (note.body.toLowerCase().includes(lower)) { fields.push('body'); } if (note.tags.some(tag => tag.toLowerCase().includes(lower))) { fields.push('tags'); } return fields; } async function executeSearchNotes(args: SearchNotesToolInput, req: NotesMcpRequest) { const userId = requireUserId(req); const result = await listNotes(userId, PRODUCT_ID, { workspaceId: args.workspaceId, status: args.status, tag: args.tag, search: args.query, limit: args.limit, offset: args.offset, }); return SearchNotesToolOutputSchema.parse({ query: args.query, items: result.items.map(note => ({ ...mapNoteSummary(note), matchFields: getMatchFields(note, args.query), })), total: result.total, limit: args.limit, offset: args.offset, }); } function buildDraftNote(args: CreateNoteDraftToolInput, userId: string): NoteDoc { const now = new Date().toISOString(); return { id: randomUUID(), productId: PRODUCT_ID, workspaceId: args.workspaceId, userId, title: args.title, body: args.body, status: 'draft', tags: args.tags, links: args.links, sourceType: args.sourceType, sourceUri: args.sourceUri, createdAt: now, updatedAt: now, createdBy: userId, updatedBy: userId, agentId: args.agentId, }; } async function recordCreateDraftAction( note: NoteDoc, args: CreateNoteDraftToolInput, req: NotesMcpRequest, userId: string ) { const action: NoteAgentActionDoc = { id: randomUUID(), productId: PRODUCT_ID, workspaceId: note.workspaceId, userId, noteId: note.id, actorId: args.agentId ?? userId, actorType: args.agentId ? 'agent' : 'human', toolName: NOTES_MCP_TOOL_NAMES.createDraft, actionType: 'create', state: 'proposed', reason: 'Created via MCP create_draft tool', afterSummary: `${note.title}\n\n${note.body}`.slice(0, 4000), idempotencyKey: args.idempotencyKey, correlationId: args.correlationId ?? req.id, workflowId: req.id, createdAt: note.createdAt, updatedAt: note.updatedAt, createdBy: userId, updatedBy: userId, }; await createNoteAgentAction(action); } async function executeCreateDraft(args: CreateNoteDraftToolInput, req: NotesMcpRequest) { const userId = requireUserId(req); const draft = buildDraftNote(args, userId); if (args.dryRun) { return CreateNoteDraftToolOutputSchema.parse({ dryRun: true, state: 'proposed', note: mapNote(draft), idempotencyKey: args.idempotencyKey, correlationId: args.correlationId ?? req.id, }); } const created = await createNote(draft); await recordCreateDraftAction(created, args, req, userId); return CreateNoteDraftToolOutputSchema.parse({ dryRun: false, state: 'draft', note: mapNote(created), idempotencyKey: args.idempotencyKey, correlationId: args.correlationId ?? req.id, }); } export const NotesExecutableMcpTools: Array< | NotesMcpTool | NotesMcpTool | NotesMcpTool | NotesMcpTool > = [ { ...NotesMcpToolDefinitions.list, execute: executeListNotes, }, { ...NotesMcpToolDefinitions.get, execute: executeGetNote, }, { ...NotesMcpToolDefinitions.search, execute: executeSearchNotes, }, { ...NotesMcpToolDefinitions.createDraft, execute: executeCreateDraft, }, ]; export function getNotesExecutableMcpTool(name: string) { return NotesExecutableMcpTools.find(tool => tool.name === name); }