mcp support
This commit is contained in:
161
listify-api/src/mcp/mcp-server.service.ts
Normal file
161
listify-api/src/mcp/mcp-server.service.ts
Normal 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>,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user