@critical-path/mcp 0.11.7 → 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/package.json +13 -9
- package/CHANGELOG.md +0 -429
- package/dist/bin/args.test.d.ts +0 -2
- package/dist/bin/args.test.d.ts.map +0 -1
- package/dist/bin/args.test.js +0 -24
- package/dist/bin/args.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -274
- package/dist/index.test.js.map +0 -1
- package/dist/tools/definitions.test.d.ts +0 -2
- package/dist/tools/definitions.test.d.ts.map +0 -1
- package/dist/tools/definitions.test.js +0 -24
- package/dist/tools/definitions.test.js.map +0 -1
- package/src/bin/args.test.ts +0 -28
- package/src/bin/args.ts +0 -50
- package/src/bin/cli.ts +0 -75
- package/src/index.test.ts +0 -347
- package/src/index.ts +0 -3
- package/src/server/index.ts +0 -275
- package/src/tools/definitions.test.ts +0 -29
- package/src/tools/definitions.ts +0 -472
- package/src/web/index.ts +0 -162
- package/tsconfig.json +0 -8
- package/tsconfig.tsbuildinfo +0 -1
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
package/src/server/index.ts
DELETED
|
@@ -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
|
-
|