@critical-path/mcp 0.11.6 → 0.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.test.ts DELETED
@@ -1,347 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { CriticalPathEngine, InMemoryStore } from '@critical-path/core';
3
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
- import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
5
- import {
6
- ALL_TOOLS,
7
- TOOL_MAP,
8
- createCriticalPathMcpServer,
9
- registerWebMcpTools,
10
- ensureModelContextShim
11
- } from './index.js';
12
-
13
- describe('@critical-path/mcp', () => {
14
- let engine: CriticalPathEngine;
15
-
16
- beforeEach(() => {
17
- engine = new CriticalPathEngine({ store: new InMemoryStore() });
18
- });
19
-
20
- describe('Tool Definitions', () => {
21
- it('registers all core project management tools', () => {
22
- const toolNames = ALL_TOOLS.map((t) => t.name);
23
- expect(toolNames).toContain('list_projects');
24
- expect(toolNames).toContain('get_project');
25
- expect(toolNames).toContain('create_project');
26
- expect(toolNames).toContain('list_tasks');
27
- expect(toolNames).toContain('get_task');
28
- expect(toolNames).toContain('create_task');
29
- expect(toolNames).toContain('update_task');
30
- expect(toolNames).toContain('delete_task');
31
- expect(toolNames).toContain('list_deliverables');
32
- expect(toolNames).toContain('create_deliverable');
33
- expect(toolNames).toContain('list_comments');
34
- expect(toolNames).toContain('add_comment');
35
- expect(toolNames).toContain('calculate_critical_path');
36
- expect(toolNames).toContain('get_timeline_ladder');
37
- expect(toolNames).toContain('get_task_ladder');
38
- expect(toolNames).toContain('get_workload_distribution');
39
- });
40
-
41
- it('executes project and task workflows through tool definitions', async () => {
42
- // 1. Create project
43
- const createProjTool = TOOL_MAP.get('create_project')!;
44
- const project = await createProjTool.execute(
45
- { name: 'Core Engine Dev', key: 'CED' },
46
- engine
47
- );
48
- expect(project.id).toBeDefined();
49
- expect(project.name).toBe('Core Engine Dev');
50
-
51
- // 2. List projects
52
- const listProjTool = TOOL_MAP.get('list_projects')!;
53
- const projects = await listProjTool.execute({}, engine);
54
- expect(projects).toHaveLength(1);
55
- expect(projects[0].id).toBe(project.id);
56
-
57
- // 3. Create task
58
- const createTaskTool = TOOL_MAP.get('create_task')!;
59
- const task = await createTaskTool.execute(
60
- {
61
- projectId: project.id,
62
- title: 'Implement WebMCP Spec',
63
- priority: 'urgent',
64
- tags: ['agent', 'mcp']
65
- },
66
- engine
67
- );
68
- expect(task.id).toBeDefined();
69
- expect(task.title).toBe('Implement WebMCP Spec');
70
- expect(task.priority).toBe('urgent');
71
-
72
- // 4. Update task
73
- const updateTaskTool = TOOL_MAP.get('update_task')!;
74
- const updated = await updateTaskTool.execute(
75
- {
76
- id: task.id,
77
- status: 'in_progress',
78
- estimatedHours: 5
79
- },
80
- engine
81
- );
82
- expect(updated.status).toBe('in_progress');
83
- expect(updated.estimatedHours).toBe(5);
84
-
85
- // 5. Add comment
86
- const addCommentTool = TOOL_MAP.get('add_comment')!;
87
- const comment = await addCommentTool.execute(
88
- {
89
- taskId: task.id,
90
- content: 'Started drafting the WebMCP handler',
91
- authorId: 'agent-1'
92
- },
93
- engine
94
- );
95
- expect(comment.content).toBe('Started drafting the WebMCP handler');
96
-
97
- // 6. List tasks with filter
98
- const listTasksTool = TOOL_MAP.get('list_tasks')!;
99
- const filtered = await listTasksTool.execute(
100
- { projectId: project.id, status: 'in_progress' },
101
- engine
102
- );
103
- expect(filtered.tasks).toHaveLength(1);
104
- expect(filtered.nextCursor).toBeUndefined();
105
-
106
- // 7. Delete task
107
- const deleteTaskTool = TOOL_MAP.get('delete_task')!;
108
- const delRes = await deleteTaskTool.execute({ id: task.id }, engine);
109
- expect(delRes.success).toBe(true);
110
-
111
- const afterDelete = await listTasksTool.execute({ projectId: project.id }, engine);
112
- expect(afterDelete.tasks).toHaveLength(0);
113
- });
114
-
115
- it('calculates critical path and returns timeline ladder via MCP tools', async () => {
116
- const createProjTool = TOOL_MAP.get('create_project')!;
117
- const project = await createProjTool.execute({ name: 'Timeline Ladder MCP', key: 'TLM' }, engine);
118
-
119
- const createTaskTool = TOOL_MAP.get('create_task')!;
120
- const t1 = await createTaskTool.execute({
121
- projectId: project.id,
122
- title: 'Step 1: Prototype',
123
- status: 'done',
124
- estimatedHours: 4
125
- }, engine);
126
-
127
- const t2 = await createTaskTool.execute({
128
- projectId: project.id,
129
- title: 'Step 2: Polish',
130
- status: 'in_progress',
131
- estimatedHours: 6
132
- }, engine);
133
-
134
- await engine.addDependency({
135
- taskId: t2.id,
136
- dependsOnTaskId: t1.id,
137
- type: 'blocking'
138
- });
139
-
140
- // 1. calculate_critical_path tool
141
- const cpmTool = TOOL_MAP.get('calculate_critical_path')!;
142
- const cpmResult = await cpmTool.execute({ projectId: project.id }, engine);
143
- expect(cpmResult.totalDurationHours).toBe(10);
144
- expect(cpmResult.criticalTaskIds).toEqual([t1.id, t2.id]);
145
-
146
- // 2. get_timeline_ladder tool
147
- const ladderTool = TOOL_MAP.get('get_timeline_ladder')!;
148
- const ladderResult = await ladderTool.execute({ projectId: project.id, level: 'all' }, engine);
149
- expect(ladderResult.macro).toBeDefined();
150
- expect(ladderResult.macro.criticalPathDurationHours).toBe(10);
151
- expect(ladderResult.standard.tasks).toHaveLength(2);
152
-
153
- // 3. get_task_ladder tool
154
- const taskLadderTool = TOOL_MAP.get('get_task_ladder')!;
155
- const taskLadderResult = await taskLadderTool.execute({ taskId: t2.id }, engine);
156
- expect(taskLadderResult.taskId).toBe(t2.id);
157
- expect(taskLadderResult.standard.title).toBe('Step 2: Polish');
158
- expect(taskLadderResult.metrics).toBeDefined();
159
-
160
- // 4. get_task_metrics tool
161
- const taskMetricsTool = TOOL_MAP.get('get_task_metrics')!;
162
- const taskMetricsResult = await taskMetricsTool.execute({ taskId: t2.id }, engine);
163
- expect(taskMetricsResult.taskId).toBe(t2.id);
164
- expect(taskMetricsResult.realityDelta.estimatedHours).toBe(6);
165
- expect(taskMetricsResult.progress).toBeDefined();
166
- expect(taskMetricsResult.evm).toBeDefined();
167
-
168
- // 5. get_task_progress_history tool
169
- const taskHistoryTool = TOOL_MAP.get('get_task_progress_history')!;
170
- const taskHistoryResult = await taskHistoryTool.execute({ taskId: t2.id }, engine);
171
- expect(taskHistoryResult.taskId).toBe(t2.id);
172
- expect(taskHistoryResult.points.length).toBeGreaterThanOrEqual(1);
173
-
174
- // 6. get_workload_distribution tool
175
- const workloadTool = TOOL_MAP.get('get_workload_distribution')!;
176
- const workloadResult = await workloadTool.execute({ projectId: project.id, interval: 'week' }, engine);
177
- expect(workloadResult.projectId).toBe(project.id);
178
- expect(workloadResult.buckets.length).toBeGreaterThan(0);
179
- expect(workloadResult.totalHours).toBeGreaterThan(0);
180
- });
181
- });
182
-
183
- describe('Standard MCP Server Creation', () => {
184
- it('creates an MCP Server instance with capabilities', () => {
185
- const server = createCriticalPathMcpServer({
186
- engine,
187
- name: 'test-mcp-server',
188
- version: '1.0.0'
189
- });
190
- expect(server).toBeDefined();
191
- });
192
-
193
- it('throws if neither engine nor client is provided', () => {
194
- expect(() => createCriticalPathMcpServer({} as any)).toThrow(
195
- 'Either engine or client must be provided'
196
- );
197
- });
198
- async function connect(options: Parameters<typeof createCriticalPathMcpServer>[0]) {
199
- const server = createCriticalPathMcpServer(options);
200
- const client = new Client({ name: 'test-client', version: '1.0.0' });
201
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
202
- await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
203
- return client;
204
- }
205
-
206
- it('refuses to call tools excluded by the tools option', async () => {
207
- const project = await engine.createProject({ name: 'Guarded', key: 'GRD' });
208
- const task = await engine.createTask({ projectId: project.id, title: 'Keep me' });
209
- const client = await connect({ engine, tools: ['list_tasks'] });
210
-
211
- const { tools } = await client.listTools();
212
- expect(tools.map((t) => t.name)).toEqual(['list_tasks']);
213
-
214
- await expect(client.callTool({ name: 'delete_task', arguments: { id: task.id } })).rejects.toThrow(
215
- /not registered/
216
- );
217
- expect(await engine.getTask(task.id)).not.toBeNull();
218
- });
219
-
220
- it('validates tool arguments and rejects undeclared fields', async () => {
221
- const source = await engine.createProject({ name: 'Source', key: 'SRC' });
222
- const other = await engine.createProject({ name: 'Other', key: 'OTH' });
223
- const task = await engine.createTask({ projectId: source.id, title: 'Stay put' });
224
- const client = await connect({ engine });
225
-
226
- const invalid = await client.callTool({ name: 'get_task', arguments: { id: 42 } });
227
- expect(invalid.isError).toBe(true);
228
- expect((invalid.content as Array<{ text: string }>)[0].text).toContain('Invalid arguments');
229
-
230
- const undeclared = await client.callTool({
231
- name: 'update_task',
232
- arguments: { id: task.id, title: 'Renamed', projectId: other.id }
233
- });
234
- expect(undeclared.isError).toBe(true);
235
- expect((undeclared.content as Array<{ text: string }>)[0].text).toContain('projectId');
236
- const unchanged = await engine.getTask(task.id);
237
- expect(unchanged?.title).toBe('Stay put');
238
- expect(unchanged?.projectId).toBe(source.id);
239
- });
240
-
241
- it('attributes engine writes to the configured actor', async () => {
242
- const project = await engine.createProject({ name: 'Actors', key: 'ACT' });
243
- const task = await engine.createTask({ projectId: project.id, title: 'T' });
244
-
245
- const defaultClient = await connect({ engine });
246
- const first = await defaultClient.callTool({ name: 'add_comment', arguments: { taskId: task.id, content: 'hi' } });
247
- expect(JSON.parse((first.content as Array<{ text: string }>)[0].text).authorId).toBe('mcp-agent');
248
-
249
- const namedClient = await connect({ engine, actor: { userId: 'claude', actorType: 'agent' } });
250
- const second = await namedClient.callTool({ name: 'add_comment', arguments: { taskId: task.id, content: 'hi' } });
251
- expect(JSON.parse((second.content as Array<{ text: string }>)[0].text).authorId).toBe('claude');
252
- });
253
-
254
- it('marks destructive tools with MCP annotations', async () => {
255
- const client = await connect({ engine });
256
- const { tools } = await client.listTools();
257
- expect(tools.find((t) => t.name === 'delete_task')?.annotations?.destructiveHint).toBe(true);
258
- expect(tools.find((t) => t.name === 'list_tasks')?.annotations?.readOnlyHint).toBe(true);
259
- });
260
- });
261
-
262
- describe('WebMCP Client Layer', () => {
263
- let mockDoc: any;
264
- let mockWin: any;
265
-
266
- beforeEach(() => {
267
- mockDoc = {};
268
- mockWin = {};
269
- });
270
-
271
- it('creates a modelContext shim if document.modelContext is not natively present', () => {
272
- const shim = ensureModelContextShim(mockDoc);
273
- expect(shim).toBeDefined();
274
- expect(mockDoc.modelContext).toBe(shim);
275
- expect(typeof shim!.registerTool).toBe('function');
276
- });
277
-
278
- it('registers tools into modelContext and allows execution', async () => {
279
- const project = await engine.createProject({ name: 'WebMCP Test Project' });
280
-
281
- // Mock client that delegates to engine
282
- const mockClient: any = {
283
- getProjects: () => engine.getProjects(),
284
- getProject: (id: string) => engine.getProject(id),
285
- createProject: (d: any) => engine.createProject(d),
286
- getTasks: (pId?: string) => engine.getTasks(pId),
287
- getTask: (id: string) => engine.getTask(id),
288
- createTask: (d: any) => engine.createTask(d),
289
- updateTask: (id: string, u: any) => engine.updateTask(id, u),
290
- deleteTask: (id: string) => engine.deleteTask(id)
291
- };
292
-
293
- const handle = registerWebMcpTools({
294
- client: mockClient,
295
- projectId: project.id,
296
- document: mockDoc,
297
- window: mockWin
298
- });
299
-
300
- expect(handle.getRegisteredTools().length).toBeGreaterThan(0);
301
- expect(mockDoc.modelContext).toBeDefined();
302
-
303
- const registered = await mockDoc.modelContext.getTools();
304
- expect(registered.length).toBe(handle.getRegisteredTools().length);
305
-
306
- // Execute create_task via WebMCP modelContext, inheriting ambient projectId
307
- const createdTask = await mockDoc.modelContext.executeTool('create_task', {
308
- title: 'Task Created via WebMCP'
309
- });
310
- expect(createdTask.id).toBeDefined();
311
- expect(createdTask.projectId).toBe(project.id);
312
- expect(createdTask.title).toBe('Task Created via WebMCP');
313
-
314
- // Verify window fallback
315
- expect(mockWin.__CRITICAL_PATH_WEBMCP__).toBeDefined();
316
- expect(mockWin.__CRITICAL_PATH_WEBMCP__.projectId).toBe(project.id);
317
-
318
- // Cleanup
319
- handle.unregister();
320
- const afterUnregister = await mockDoc.modelContext.getTools();
321
- expect(afterUnregister).toHaveLength(0);
322
- expect(mockWin.__CRITICAL_PATH_WEBMCP__).toBeUndefined();
323
- });
324
-
325
- it('supports AbortSignal unregistration lifecycle', async () => {
326
- const controller = new AbortController();
327
- const mockClient: any = {
328
- getTasks: () => Promise.resolve([])
329
- };
330
-
331
- registerWebMcpTools({
332
- client: mockClient,
333
- signal: controller.signal,
334
- document: mockDoc,
335
- window: mockWin
336
- });
337
-
338
- let tools = await mockDoc.modelContext.getTools();
339
- expect(tools.length).toBeGreaterThan(0);
340
-
341
- controller.abort();
342
-
343
- tools = await mockDoc.modelContext.getTools();
344
- expect(tools).toHaveLength(0);
345
- });
346
- });
347
- });
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './tools/definitions.js';
2
- export * from './server/index.js';
3
- export * from './web/index.js';
@@ -1,275 +0,0 @@
1
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- ListResourcesRequestSchema,
7
- ReadResourceRequestSchema,
8
- ListPromptsRequestSchema,
9
- GetPromptRequestSchema,
10
- ErrorCode,
11
- McpError
12
- } from '@modelcontextprotocol/sdk/types.js';
13
- import type { Actor, CriticalPathEngine } from '@critical-path/core';
14
- import type { CriticalPathClient } from '@critical-path/client';
15
- import { ALL_TOOLS, parseToolArgs, type BackendContext } from '../tools/definitions.js';
16
-
17
- export interface CriticalPathMcpServerOptions {
18
- engine?: CriticalPathEngine;
19
- client?: CriticalPathClient;
20
- name?: string;
21
- version?: string;
22
- tools?: string[];
23
- /**
24
- * Identity that changes made through an `engine` are attributed to (activity log, comment
25
- * authors, ...). Defaults to `DEFAULT_MCP_ACTOR`. With a `client`, identity comes from the API's
26
- * authentication (e.g. the `Authorization` header passed to the client) instead.
27
- */
28
- actor?: Actor;
29
- }
30
-
31
- export const DEFAULT_MCP_ACTOR: Actor = Object.freeze({ userId: 'mcp-agent', username: 'MCP Agent', actorType: 'agent' });
32
-
33
- export function createCriticalPathMcpServer(options: CriticalPathMcpServerOptions): Server {
34
- const target: BackendContext | undefined = options.engine
35
- ? options.engine.withActor(options.actor ?? DEFAULT_MCP_ACTOR)
36
- : options.client;
37
- if (!target) {
38
- throw new Error('Either engine or client must be provided to createCriticalPathMcpServer.');
39
- }
40
-
41
- const serverName = options.name || 'critical-path-mcp';
42
- const serverVersion = options.version || '0.1.0';
43
-
44
- const activeTools = options.tools
45
- ? ALL_TOOLS.filter((t) => options.tools!.includes(t.name))
46
- : ALL_TOOLS;
47
- const activeToolMap = new Map(activeTools.map((tool) => [tool.name, tool]));
48
-
49
- const server = new Server(
50
- {
51
- name: serverName,
52
- version: serverVersion
53
- },
54
- {
55
- capabilities: {
56
- tools: {},
57
- resources: {},
58
- prompts: {}
59
- }
60
- }
61
- );
62
-
63
- // 1. Tool Listing
64
- server.setRequestHandler(ListToolsRequestSchema, async () => {
65
- return {
66
- tools: activeTools.map((tool) => ({
67
- name: tool.name,
68
- title: tool.title,
69
- description: tool.description,
70
- inputSchema: tool.inputSchema,
71
- annotations: {
72
- title: tool.title,
73
- readOnlyHint: tool.annotations?.readOnlyHint ?? false,
74
- destructiveHint: tool.annotations?.requiresConfirmation ?? false
75
- }
76
- }))
77
- };
78
- });
79
-
80
- // 2. Tool Execution
81
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
82
- const { name, arguments: args } = request.params;
83
- // Look up only the tools this server exposes, so a filtered-out tool cannot be called by name.
84
- const tool = activeToolMap.get(name);
85
-
86
- if (!tool) {
87
- throw new McpError(ErrorCode.MethodNotFound, `Tool "${name}" is not registered on this MCP server.`);
88
- }
89
-
90
- try {
91
- // Plugin init and seeding must finish before tools touch the engine.
92
- if (options.engine) await options.engine.ready;
93
- const result = await tool.execute(parseToolArgs(tool, args), target);
94
- return {
95
- content: [
96
- {
97
- type: 'text',
98
- text: JSON.stringify(result, null, 2)
99
- }
100
- ]
101
- };
102
- } catch (err: any) {
103
- return {
104
- content: [
105
- {
106
- type: 'text',
107
- text: `Error executing tool "${name}": ${err?.message || String(err)}`
108
- }
109
- ],
110
- isError: true
111
- };
112
- }
113
- });
114
-
115
- // 3. Resources Listing
116
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
117
- return {
118
- resources: [
119
- {
120
- uri: 'criticalpath://projects',
121
- name: 'Critical Path Projects',
122
- mimeType: 'application/json',
123
- description: 'Live list of all projects managed by Critical Path'
124
- },
125
- {
126
- uri: 'criticalpath://tasks',
127
- name: 'Critical Path Tasks',
128
- mimeType: 'application/json',
129
- description: 'Live list of all tasks across projects'
130
- }
131
- ]
132
- };
133
- });
134
-
135
- // 4. Resource Reading
136
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
137
- const uri = request.params.uri;
138
-
139
- if (uri === 'criticalpath://projects') {
140
- const projects = await target.getProjects();
141
- return {
142
- contents: [
143
- {
144
- uri,
145
- mimeType: 'application/json',
146
- text: JSON.stringify(projects, null, 2)
147
- }
148
- ]
149
- };
150
- }
151
-
152
- if (uri === 'criticalpath://tasks') {
153
- const tasks = await target.getTasks();
154
- return {
155
- contents: [
156
- {
157
- uri,
158
- mimeType: 'application/json',
159
- text: JSON.stringify(tasks, null, 2)
160
- }
161
- ]
162
- };
163
- }
164
-
165
- const taskMatch = uri.match(/^criticalpath:\/\/tasks\/([^/]+)$/);
166
- if (taskMatch) {
167
- const taskId = taskMatch[1];
168
- const task = await target.getTask(taskId);
169
- if (!task) {
170
- throw new McpError(ErrorCode.InvalidRequest, `Task with ID "${taskId}" not found.`);
171
- }
172
- return {
173
- contents: [
174
- {
175
- uri,
176
- mimeType: 'application/json',
177
- text: JSON.stringify(task, null, 2)
178
- }
179
- ]
180
- };
181
- }
182
-
183
- throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`);
184
- });
185
-
186
- // 5. Prompts Listing
187
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
188
- return {
189
- prompts: [
190
- {
191
- name: 'summarize_project',
192
- description: 'Analyze project health, open tasks, blockers, and recent deliverables.',
193
- arguments: [
194
- {
195
- name: 'projectId',
196
- description: 'The ID of the project to summarize',
197
- required: true
198
- }
199
- ]
200
- },
201
- {
202
- name: 'triage_task',
203
- description: 'Review a task description, suggest priority, labels, and workflow status.',
204
- arguments: [
205
- {
206
- name: 'taskId',
207
- description: 'The task ID to triage',
208
- required: true
209
- }
210
- ]
211
- }
212
- ]
213
- };
214
- });
215
-
216
- // 6. Prompts Retrieval
217
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
218
- const { name, arguments: args } = request.params;
219
-
220
- if (name === 'summarize_project') {
221
- const projectId = args?.projectId;
222
- if (!projectId) {
223
- throw new McpError(ErrorCode.InvalidParams, 'Argument "projectId" is required.');
224
- }
225
- const project = await target.getProject(projectId);
226
- const tasks = await target.getTasks(projectId);
227
-
228
- return {
229
- description: `Project summary for ${project?.name || projectId}`,
230
- messages: [
231
- {
232
- role: 'user',
233
- content: {
234
- type: 'text',
235
- text: `Please analyze the current status of project "${project?.name || projectId}".\n\nProject Info:\n${JSON.stringify(project, null, 2)}\n\nTasks (${tasks.length} total):\n${JSON.stringify(tasks, null, 2)}\n\nProvide an executive summary highlighting:\n1. Overall progress and completion rate\n2. Any high/urgent priority tasks needing immediate attention\n3. Recommendations for unblocking or advancing the sprint.`
236
- }
237
- }
238
- ]
239
- };
240
- }
241
-
242
- if (name === 'triage_task') {
243
- const taskId = args?.taskId;
244
- if (!taskId) {
245
- throw new McpError(ErrorCode.InvalidParams, 'Argument "taskId" is required.');
246
- }
247
- const task = await target.getTask(taskId);
248
- if (!task) {
249
- throw new McpError(ErrorCode.InvalidRequest, `Task "${taskId}" not found.`);
250
- }
251
-
252
- return {
253
- description: `Triage task "${task.title}"`,
254
- messages: [
255
- {
256
- role: 'user',
257
- content: {
258
- type: 'text',
259
- text: `Please review and triage the following task:\n\n${JSON.stringify(task, null, 2)}\n\nSuggest:\n1. Appropriate priority level\n2. Relevant tags\n3. Estimated completion hours\n4. Clear acceptance criteria or missing details.`
260
- }
261
- }
262
- ]
263
- };
264
- }
265
-
266
- throw new McpError(ErrorCode.MethodNotFound, `Prompt "${name}" is not defined.`);
267
- });
268
-
269
- return server;
270
- }
271
-
272
- export async function startStdioServer(server: Server): Promise<void> {
273
- const transport = new StdioServerTransport();
274
- await server.connect(transport);
275
- }
@@ -1,29 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from './definitions.js';
3
-
4
- describe('MCP tool definitions', () => {
5
- // inputSchema is generated from zodSchema by defineTool; this guards the generation itself
6
- // (e.g. a zod feature that zod-to-json-schema cannot express would drop fields here).
7
- it.each(ALL_TOOLS.map((t) => [t.name, t] as const))('%s: zodSchema and inputSchema agree', (_name, tool) => {
8
- const json = tool.inputSchema as { properties?: Record<string, unknown>; required?: string[] };
9
- const shape = tool.zodSchema.shape as Record<string, { isOptional(): boolean }>;
10
-
11
- expect(Object.keys(shape).sort()).toEqual(Object.keys(json.properties ?? {}).sort());
12
- expect(
13
- Object.entries(shape)
14
- .filter(([, schema]) => !schema.isOptional())
15
- .map(([key]) => key)
16
- .sort()
17
- ).toEqual([...(json.required ?? [])].sort());
18
- });
19
-
20
- it('generates closed object schemas with descriptions for every tool', () => {
21
- for (const tool of ALL_TOOLS) {
22
- expect(tool.inputSchema).toMatchObject({ type: 'object', additionalProperties: false });
23
- expect(tool.inputSchema).not.toHaveProperty('$schema');
24
- }
25
- const updateTask = ALL_TOOLS.find((t) => t.name === 'update_task')!;
26
- expect((updateTask.inputSchema as any).properties.isBlocked.description).toBe('Whether the task is blocked');
27
- });
28
- });
29
-