chat
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { ListTemplatesService } from '../list-templates/list-templates.service';
|
||||
import { UserList } from '../list-templates/list-template.types';
|
||||
import { ListTemplate, UserList } from '../list-templates/list-template.types';
|
||||
import { ListsService } from '../lists/lists.service';
|
||||
import { ListSuggestionAgentService } from './list-suggestion-agent.service';
|
||||
import { McpServerService } from './mcp-server.service';
|
||||
|
||||
describe('McpServerService', () => {
|
||||
let listsService: Pick<ListsService, 'listLists' | 'createList' | 'addItem'>;
|
||||
let listTemplatesService: Pick<ListTemplatesService, 'listTemplates'>;
|
||||
let listTemplatesService: Pick<
|
||||
ListTemplatesService,
|
||||
'listTemplates' | 'createTemplate' | 'addItem'
|
||||
>;
|
||||
let listSuggestionAgentService: Pick<
|
||||
ListSuggestionAgentService,
|
||||
'suggestLists'
|
||||
@@ -21,6 +24,8 @@ describe('McpServerService', () => {
|
||||
};
|
||||
listTemplatesService = {
|
||||
listTemplates: jest.fn(),
|
||||
createTemplate: jest.fn(),
|
||||
addItem: jest.fn(),
|
||||
};
|
||||
listSuggestionAgentService = {
|
||||
suggestLists: jest.fn(),
|
||||
@@ -142,6 +147,87 @@ describe('McpServerService', () => {
|
||||
});
|
||||
expect(result.structuredContent).toEqual({ list: updatedList });
|
||||
});
|
||||
|
||||
it('registers create_template as a write tool and creates initial items', async () => {
|
||||
const createdTemplate = template({
|
||||
id: 'template-1',
|
||||
name: 'Urlaub',
|
||||
items: ['Pass', 'Tickets'],
|
||||
});
|
||||
jest
|
||||
.mocked(listTemplatesService.createTemplate)
|
||||
.mockResolvedValue(createdTemplate);
|
||||
|
||||
const tool = toolFrom(service.createServer('user-1'), 'create_template');
|
||||
const result = await tool.handler(
|
||||
{
|
||||
name: 'Urlaub',
|
||||
description: 'Packvorlage',
|
||||
kind: 'packing',
|
||||
items: [
|
||||
{ title: 'Pass', required: true },
|
||||
{ title: 'Tickets', notes: 'Digital sichern', required: false },
|
||||
],
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(tool.annotations).toEqual(
|
||||
expect.objectContaining({
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
}),
|
||||
);
|
||||
expect(listTemplatesService.createTemplate).toHaveBeenCalledWith('user-1', {
|
||||
name: 'Urlaub',
|
||||
description: 'Packvorlage',
|
||||
kind: 'packing',
|
||||
items: [
|
||||
{ title: 'Pass', required: true },
|
||||
{ title: 'Tickets', notes: 'Digital sichern', required: false },
|
||||
],
|
||||
});
|
||||
expect(result.structuredContent).toEqual({ template: createdTemplate });
|
||||
});
|
||||
|
||||
it('registers add_template_item as a write tool and adds an item', async () => {
|
||||
const updatedTemplate = template({
|
||||
id: 'template-1',
|
||||
name: 'Urlaub',
|
||||
items: ['Pass'],
|
||||
});
|
||||
jest.mocked(listTemplatesService.addItem).mockResolvedValue(updatedTemplate);
|
||||
|
||||
const tool = toolFrom(service.createServer('user-1'), 'add_template_item');
|
||||
const result = await tool.handler(
|
||||
{
|
||||
templateId: 'template-1',
|
||||
title: 'Pass',
|
||||
quantity: 1,
|
||||
},
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(tool.annotations).toEqual(
|
||||
expect.objectContaining({
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
}),
|
||||
);
|
||||
expect(listTemplatesService.addItem).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
'template-1',
|
||||
{
|
||||
title: 'Pass',
|
||||
notes: undefined,
|
||||
quantity: 1,
|
||||
required: undefined,
|
||||
},
|
||||
);
|
||||
expect(result.structuredContent).toEqual({ template: updatedTemplate });
|
||||
});
|
||||
});
|
||||
|
||||
function toolFrom(server: object, name: string) {
|
||||
@@ -181,6 +267,29 @@ function list(options: {
|
||||
};
|
||||
}
|
||||
|
||||
function template(options: {
|
||||
id: string;
|
||||
name: string;
|
||||
items?: string[];
|
||||
}): ListTemplate {
|
||||
return {
|
||||
id: options.id,
|
||||
ownerId: 'user-1',
|
||||
name: options.name,
|
||||
kind: 'packing',
|
||||
items: (options.items ?? []).map((title, position) => ({
|
||||
id: `template-item-${position}`,
|
||||
title,
|
||||
required: true,
|
||||
position,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
})),
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
}
|
||||
|
||||
function now(): string {
|
||||
return new Date(0).toISOString();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,24 @@ const listItemInputSchema = {
|
||||
.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 {
|
||||
@@ -225,6 +243,79 @@ export class McpServerService {
|
||||
},
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user