mcp support

This commit is contained in:
Bastian Wagner
2026-06-11 11:19:07 +02:00
parent e1cc78ca27
commit c8603be226
11 changed files with 1116 additions and 9 deletions

View File

@@ -0,0 +1,161 @@
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();
@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);
},
);
return server;
}
private toToolResult(data: object) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(data, null, 2),
},
],
structuredContent: data as Record<string, unknown>,
};
}
}