import { Injectable } from '@nestjs/common'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import * as z from 'zod/v4'; import { ListTemplateKind } from '../list-templates/list-template.types'; import { ListTemplatesService } from '../list-templates/list-templates.service'; import { ListsService } from '../lists/lists.service'; import { ListSuggestionAgentService } from './list-suggestion-agent.service'; const listKindSchema = z .enum(['packing', 'shopping', 'todo', 'custom']) .optional(); const listItemInputSchema = { title: z.string().trim().min(1).describe('List item title.'), notes: z.string().trim().min(1).optional().describe('Optional item notes.'), quantity: z.number().positive().optional().describe('Optional item quantity.'), required: z .boolean() .optional() .describe('Whether the item is required. Defaults to true.'), }; const templateItemInputSchema = { title: z.string().trim().min(1).describe('Template item title.'), notes: z .string() .trim() .min(1) .optional() .describe('Optional template item notes.'), quantity: z .number() .positive() .optional() .describe('Optional template item quantity.'), required: z .boolean() .optional() .describe('Whether the template item is required. Defaults to true.'), }; @Injectable() export class McpServerService { constructor( private readonly listsService: ListsService, private readonly listTemplatesService: ListTemplatesService, private readonly listSuggestionAgentService: ListSuggestionAgentService, ) {} createServer(userId: string): McpServer { const server = new McpServer({ name: 'listify', version: '1.0.0', }); server.registerTool( 'list_existing_lists', { title: 'List existing lists', description: 'Returns the authenticated user lists. This tool is read-only.', inputSchema: { includeItems: z .boolean() .optional() .describe('Whether to include list items in the response.'), }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, async ({ includeItems = false }) => { const lists = await this.listsService.listLists(userId); const result = { lists: lists.map((list) => ({ id: list.id, name: list.name, description: list.description, kind: list.kind, accessRole: list.accessRole, itemCount: list.items.length, items: includeItems ? list.items.map((item) => ({ id: item.id, title: item.title, notes: item.notes, quantity: item.quantity, required: item.required, checked: item.checked, position: item.position, })) : undefined, })), }; return this.toToolResult(result); }, ); server.registerTool( 'list_templates', { title: 'List templates', description: 'Returns the authenticated user list templates. This tool is read-only.', inputSchema: { kind: listKindSchema.describe('Optional template kind filter.'), }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, async ({ kind }) => { const templates = await this.listTemplatesService.listTemplates(userId); const result = { templates: templates .filter((template) => !kind || template.kind === kind) .map((template) => ({ id: template.id, name: template.name, description: template.description, kind: template.kind, items: template.items.map((item) => ({ id: item.id, title: item.title, notes: item.notes, quantity: item.quantity, required: item.required, position: item.position, })), })), }; return this.toToolResult(result); }, ); server.registerTool( 'suggest_lists', { title: 'Suggest lists', description: 'Suggests new lists for the authenticated user without creating or modifying data.', inputSchema: { goal: z.string().min(1).describe('What the user wants a list for.'), kind: listKindSchema.describe('Optional desired list kind.'), constraints: z .array(z.string().min(1)) .optional() .describe('Optional constraints or must-have list items.'), }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, }, }, async ({ goal, kind, constraints }) => { const result = await this.listSuggestionAgentService.suggestLists( userId, { goal, kind: kind as ListTemplateKind | undefined, constraints, }, ); return this.toToolResult(result); }, ); server.registerTool( 'create_list', { title: 'Create list', description: 'Creates a new list for the authenticated user and optionally adds initial items.', inputSchema: { name: z.string().trim().min(1).describe('List name.'), description: z .string() .trim() .min(1) .optional() .describe('Optional list description.'), kind: listKindSchema.describe('Optional list kind.'), items: z .array(z.object(listItemInputSchema)) .max(50) .optional() .describe('Optional initial list items.'), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ name, description, kind, items = [] }) => { let list = await this.listsService.createList(userId, { name, description, kind: kind as ListTemplateKind | undefined, }); for (const item of items) { list = await this.listsService.addItem(userId, list.id, item); } return this.toToolResult({ list }); }, ); server.registerTool( 'add_list_item', { title: 'Add list item', description: 'Adds an item to an existing list the authenticated user can access.', inputSchema: { listId: z.string().trim().min(1).describe('Target list id.'), ...listItemInputSchema, }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ listId, title, notes, quantity, required }) => { const list = await this.listsService.addItem(userId, listId, { title, notes, quantity, required, }); return this.toToolResult({ list }); }, ); server.registerTool( 'create_template', { title: 'Create template', description: 'Creates a new list template for the authenticated user and optionally adds template items.', inputSchema: { name: z.string().trim().min(1).describe('Template name.'), description: z .string() .trim() .min(1) .optional() .describe('Optional template description.'), kind: listKindSchema.describe('Optional template kind.'), items: z .array(z.object(templateItemInputSchema)) .max(50) .optional() .describe('Optional initial template items.'), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ name, description, kind, items = [] }) => { const template = await this.listTemplatesService.createTemplate(userId, { name, description, kind: kind as ListTemplateKind | undefined, items, }); return this.toToolResult({ template }); }, ); server.registerTool( 'add_template_item', { title: 'Add template item', description: 'Adds an item to an existing list template owned by the authenticated user.', inputSchema: { templateId: z.string().trim().min(1).describe('Target template id.'), ...templateItemInputSchema, }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, }, async ({ templateId, title, notes, quantity, required }) => { const template = await this.listTemplatesService.addItem( userId, templateId, { title, notes, quantity, required, }, ); return this.toToolResult({ template }); }, ); return server; } private toToolResult(data: object) { return { content: [ { type: 'text' as const, text: JSON.stringify(data, null, 2), }, ], structuredContent: data as Record, }; } }