@artyfacts/mcp-server 1.0.0

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.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @artyfacts/mcp-server
3
+ *
4
+ * MCP (Model Context Protocol) server that exposes Artyfacts tools
5
+ * for use with Claude Code and other MCP-compatible clients.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createMcpServer, startServer } from '@artyfacts/mcp-server';
10
+ *
11
+ * const server = createMcpServer({
12
+ * apiKey: 'your-api-key',
13
+ * baseUrl: 'https://artyfacts.dev/api/v1',
14
+ * });
15
+ *
16
+ * // Start as stdio server (for MCP)
17
+ * startServer(server);
18
+ * ```
19
+ */
20
+
21
+ export { createMcpServer, ArtyfactsMcpServer } from './server';
22
+ export type { McpServerConfig } from './server';
23
+
24
+ // Re-export for convenience
25
+ export { startServer } from './server';
package/src/server.ts ADDED
@@ -0,0 +1,605 @@
1
+ /**
2
+ * Artyfacts MCP Server Implementation
3
+ *
4
+ * Exposes Artyfacts tools via MCP (Model Context Protocol) for use
5
+ * with Claude Code and other MCP-compatible clients.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ CallToolRequestSchema,
12
+ ListToolsRequestSchema,
13
+ Tool,
14
+ } from '@modelcontextprotocol/sdk/types.js';
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export interface McpServerConfig {
21
+ /** Artyfacts API key */
22
+ apiKey: string;
23
+ /** Artyfacts API base URL */
24
+ baseUrl?: string;
25
+ /** Server name */
26
+ name?: string;
27
+ /** Server version */
28
+ version?: string;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Tool Definitions
33
+ // ============================================================================
34
+
35
+ const ARTYFACTS_TOOLS: Tool[] = [
36
+ // Organization tools
37
+ {
38
+ name: 'get_organization',
39
+ description: 'Get details about the current organization',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {},
43
+ required: [],
44
+ },
45
+ },
46
+
47
+ // Project tools
48
+ {
49
+ name: 'list_projects',
50
+ description: 'List all projects in the organization',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ limit: { type: 'number', description: 'Max results (default 50)' },
55
+ offset: { type: 'number', description: 'Pagination offset' },
56
+ },
57
+ required: [],
58
+ },
59
+ },
60
+ {
61
+ name: 'get_project',
62
+ description: 'Get details about a specific project',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ project_id: { type: 'string', description: 'Project ID' },
67
+ },
68
+ required: ['project_id'],
69
+ },
70
+ },
71
+
72
+ // Artifact tools
73
+ {
74
+ name: 'list_artifacts',
75
+ description: 'List artifacts, optionally filtered by project',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ project_id: { type: 'string', description: 'Filter by project' },
80
+ type: { type: 'string', description: 'Filter by type (goal, spec, research, etc.)' },
81
+ status: { type: 'string', description: 'Filter by status' },
82
+ limit: { type: 'number', description: 'Max results' },
83
+ },
84
+ required: [],
85
+ },
86
+ },
87
+ {
88
+ name: 'get_artifact',
89
+ description: 'Get a specific artifact by ID',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ artifact_id: { type: 'string', description: 'Artifact ID' },
94
+ },
95
+ required: ['artifact_id'],
96
+ },
97
+ },
98
+ {
99
+ name: 'create_artifact',
100
+ description: 'Create a new artifact',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ title: { type: 'string', description: 'Artifact title' },
105
+ type: { type: 'string', description: 'Type: goal, spec, research, report, experiment, decision' },
106
+ content: { type: 'string', description: 'Markdown content' },
107
+ project_id: { type: 'string', description: 'Project ID' },
108
+ parent_id: { type: 'string', description: 'Parent artifact ID' },
109
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
110
+ },
111
+ required: ['title'],
112
+ },
113
+ },
114
+ {
115
+ name: 'update_artifact',
116
+ description: 'Update an existing artifact',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ artifact_id: { type: 'string', description: 'Artifact ID' },
121
+ title: { type: 'string', description: 'New title' },
122
+ content: { type: 'string', description: 'New content' },
123
+ status: { type: 'string', description: 'New status' },
124
+ },
125
+ required: ['artifact_id'],
126
+ },
127
+ },
128
+
129
+ // Section tools
130
+ {
131
+ name: 'list_sections',
132
+ description: 'List sections of an artifact',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ artifact_id: { type: 'string', description: 'Artifact ID' },
137
+ },
138
+ required: ['artifact_id'],
139
+ },
140
+ },
141
+ {
142
+ name: 'get_section',
143
+ description: 'Get a specific section',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ artifact_id: { type: 'string', description: 'Artifact ID' },
148
+ section_id: { type: 'string', description: 'Section ID' },
149
+ },
150
+ required: ['artifact_id', 'section_id'],
151
+ },
152
+ },
153
+ {
154
+ name: 'create_section',
155
+ description: 'Create a new section on an artifact',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ artifact_id: { type: 'string', description: 'Artifact ID' },
160
+ section_id: { type: 'string', description: 'Section identifier (slug)' },
161
+ heading: { type: 'string', description: 'Section heading' },
162
+ content: { type: 'string', description: 'Markdown content' },
163
+ type: { type: 'string', description: 'Type: content, task, decision, blocker' },
164
+ position: { type: 'number', description: 'Order position' },
165
+ },
166
+ required: ['artifact_id', 'section_id', 'heading'],
167
+ },
168
+ },
169
+ {
170
+ name: 'update_section',
171
+ description: 'Update an existing section',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ artifact_id: { type: 'string', description: 'Artifact ID' },
176
+ section_id: { type: 'string', description: 'Section ID' },
177
+ heading: { type: 'string', description: 'New heading' },
178
+ content: { type: 'string', description: 'New content' },
179
+ task_status: { type: 'string', description: 'Task status if type=task' },
180
+ },
181
+ required: ['artifact_id', 'section_id'],
182
+ },
183
+ },
184
+ {
185
+ name: 'delete_section',
186
+ description: 'Delete a section',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ artifact_id: { type: 'string', description: 'Artifact ID' },
191
+ section_id: { type: 'string', description: 'Section ID' },
192
+ },
193
+ required: ['artifact_id', 'section_id'],
194
+ },
195
+ },
196
+
197
+ // Task tools
198
+ {
199
+ name: 'list_tasks',
200
+ description: 'List tasks (sections with type=task)',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ artifact_id: { type: 'string', description: 'Filter by artifact' },
205
+ status: { type: 'string', description: 'Filter by status: pending, in_progress, done, blocked' },
206
+ assignee: { type: 'string', description: 'Filter by assignee agent' },
207
+ },
208
+ required: [],
209
+ },
210
+ },
211
+ {
212
+ name: 'claim_task',
213
+ description: 'Claim a task for the current agent',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ task_id: { type: 'string', description: 'Task ID (section UUID)' },
218
+ },
219
+ required: ['task_id'],
220
+ },
221
+ },
222
+ {
223
+ name: 'complete_task',
224
+ description: 'Mark a task as complete',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ task_id: { type: 'string', description: 'Task ID' },
229
+ output_url: { type: 'string', description: 'URL to deliverable (PR, doc, etc.)' },
230
+ summary: { type: 'string', description: 'Completion summary' },
231
+ },
232
+ required: ['task_id'],
233
+ },
234
+ },
235
+ {
236
+ name: 'block_task',
237
+ description: 'Mark a task as blocked',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ task_id: { type: 'string', description: 'Task ID' },
242
+ reason: { type: 'string', description: 'Why it is blocked' },
243
+ blocker_type: { type: 'string', description: 'Type: decision, dependency, resource, external' },
244
+ },
245
+ required: ['task_id', 'reason'],
246
+ },
247
+ },
248
+
249
+ // Agent tools
250
+ {
251
+ name: 'list_agents',
252
+ description: 'List all agents in the organization',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {
256
+ status: { type: 'string', description: 'Filter by status: active, inactive' },
257
+ },
258
+ required: [],
259
+ },
260
+ },
261
+ {
262
+ name: 'get_agent',
263
+ description: 'Get details about an agent',
264
+ inputSchema: {
265
+ type: 'object',
266
+ properties: {
267
+ agent_id: { type: 'string', description: 'Agent ID' },
268
+ },
269
+ required: ['agent_id'],
270
+ },
271
+ },
272
+ {
273
+ name: 'create_agent',
274
+ description: 'Register a new agent',
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: {
278
+ name: { type: 'string', description: 'Agent display name' },
279
+ type: { type: 'string', description: 'Type: pm, engineering, qa, research, content, design' },
280
+ description: { type: 'string', description: 'What this agent does' },
281
+ capabilities: { type: 'array', items: { type: 'string' }, description: 'Agent capabilities' },
282
+ config: { type: 'object', description: 'Agent configuration' },
283
+ },
284
+ required: ['name', 'type'],
285
+ },
286
+ },
287
+ {
288
+ name: 'update_agent',
289
+ description: 'Update an agent',
290
+ inputSchema: {
291
+ type: 'object',
292
+ properties: {
293
+ agent_id: { type: 'string', description: 'Agent ID' },
294
+ name: { type: 'string', description: 'New name' },
295
+ status: { type: 'string', description: 'New status' },
296
+ config: { type: 'object', description: 'Updated config' },
297
+ },
298
+ required: ['agent_id'],
299
+ },
300
+ },
301
+
302
+ // Blocker tools
303
+ {
304
+ name: 'list_blockers',
305
+ description: 'List blockers (decisions, dependencies needing resolution)',
306
+ inputSchema: {
307
+ type: 'object',
308
+ properties: {
309
+ artifact_id: { type: 'string', description: 'Filter by artifact' },
310
+ status: { type: 'string', description: 'Filter by status: open, resolved' },
311
+ },
312
+ required: [],
313
+ },
314
+ },
315
+ {
316
+ name: 'create_blocker',
317
+ description: 'Create a blocker (decision request, dependency, etc.)',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ artifact_id: { type: 'string', description: 'Artifact ID' },
322
+ kind: { type: 'string', description: 'Kind: decision, dependency, resource, external' },
323
+ title: { type: 'string', description: 'Blocker title' },
324
+ description: { type: 'string', description: 'Details' },
325
+ options: { type: 'array', description: 'Options for decisions' },
326
+ blocked_tasks: { type: 'array', items: { type: 'string' }, description: 'Task IDs blocked by this' },
327
+ },
328
+ required: ['artifact_id', 'kind', 'title'],
329
+ },
330
+ },
331
+ {
332
+ name: 'resolve_blocker',
333
+ description: 'Resolve a blocker',
334
+ inputSchema: {
335
+ type: 'object',
336
+ properties: {
337
+ blocker_id: { type: 'string', description: 'Blocker ID' },
338
+ resolution: { type: 'string', description: 'How it was resolved' },
339
+ selected_option: { type: 'string', description: 'Selected option (for decisions)' },
340
+ },
341
+ required: ['blocker_id', 'resolution'],
342
+ },
343
+ },
344
+
345
+ // Search tools
346
+ {
347
+ name: 'search_artifacts',
348
+ description: 'Search artifacts by text query',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: {
352
+ query: { type: 'string', description: 'Search query' },
353
+ type: { type: 'string', description: 'Filter by type' },
354
+ limit: { type: 'number', description: 'Max results' },
355
+ },
356
+ required: ['query'],
357
+ },
358
+ },
359
+
360
+ // Context tools
361
+ {
362
+ name: 'get_task_context',
363
+ description: 'Get full context for a task (org, project, artifact, related sections)',
364
+ inputSchema: {
365
+ type: 'object',
366
+ properties: {
367
+ task_id: { type: 'string', description: 'Task ID' },
368
+ },
369
+ required: ['task_id'],
370
+ },
371
+ },
372
+ ];
373
+
374
+ // ============================================================================
375
+ // API Client
376
+ // ============================================================================
377
+
378
+ class ArtyfactsApiClient {
379
+ constructor(
380
+ private baseUrl: string,
381
+ private apiKey: string
382
+ ) {}
383
+
384
+ async request(method: string, path: string, body?: unknown): Promise<unknown> {
385
+ const url = `${this.baseUrl}${path}`;
386
+
387
+ const response = await fetch(url, {
388
+ method,
389
+ headers: {
390
+ 'Authorization': `Bearer ${this.apiKey}`,
391
+ 'Content-Type': 'application/json',
392
+ },
393
+ body: body ? JSON.stringify(body) : undefined,
394
+ });
395
+
396
+ if (!response.ok) {
397
+ const error = await response.json().catch(() => ({ error: 'Request failed' })) as { error?: string };
398
+ throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`);
399
+ }
400
+
401
+ return response.json();
402
+ }
403
+
404
+ get(path: string) { return this.request('GET', path); }
405
+ post(path: string, body?: unknown) { return this.request('POST', path, body); }
406
+ patch(path: string, body?: unknown) { return this.request('PATCH', path, body); }
407
+ delete(path: string) { return this.request('DELETE', path); }
408
+ }
409
+
410
+ // ============================================================================
411
+ // Tool Handlers
412
+ // ============================================================================
413
+
414
+ type ToolHandler = (client: ArtyfactsApiClient, args: Record<string, unknown>) => Promise<unknown>;
415
+
416
+ const toolHandlers: Record<string, ToolHandler> = {
417
+ // Organization
418
+ get_organization: (client) => client.get('/org'),
419
+
420
+ // Projects
421
+ list_projects: (client, args) => {
422
+ const params = new URLSearchParams();
423
+ if (args.limit) params.set('limit', String(args.limit));
424
+ if (args.offset) params.set('offset', String(args.offset));
425
+ return client.get(`/projects?${params}`);
426
+ },
427
+ get_project: (client, args) => client.get(`/projects/${args.project_id}`),
428
+
429
+ // Artifacts
430
+ list_artifacts: (client, args) => {
431
+ const params = new URLSearchParams();
432
+ if (args.project_id) params.set('project_id', String(args.project_id));
433
+ if (args.type) params.set('type', String(args.type));
434
+ if (args.status) params.set('status', String(args.status));
435
+ if (args.limit) params.set('limit', String(args.limit));
436
+ return client.get(`/artifacts?${params}`);
437
+ },
438
+ get_artifact: (client, args) => client.get(`/artifacts/${args.artifact_id}`),
439
+ create_artifact: (client, args) => client.post('/artifacts', args),
440
+ update_artifact: (client, args) => {
441
+ const { artifact_id, ...body } = args;
442
+ return client.patch(`/artifacts/${artifact_id}`, body);
443
+ },
444
+
445
+ // Sections
446
+ list_sections: (client, args) => client.get(`/artifacts/${args.artifact_id}/sections`),
447
+ get_section: (client, args) => client.get(`/artifacts/${args.artifact_id}/sections/${args.section_id}`),
448
+ create_section: (client, args) => {
449
+ const { artifact_id, ...body } = args;
450
+ return client.post(`/artifacts/${artifact_id}/sections`, body);
451
+ },
452
+ update_section: (client, args) => {
453
+ const { artifact_id, section_id, ...body } = args;
454
+ return client.patch(`/artifacts/${artifact_id}/sections/${section_id}`, body);
455
+ },
456
+ delete_section: (client, args) => client.delete(`/artifacts/${args.artifact_id}/sections/${args.section_id}`),
457
+
458
+ // Tasks
459
+ list_tasks: (client, args) => {
460
+ const params = new URLSearchParams();
461
+ if (args.artifact_id) params.set('artifact_id', String(args.artifact_id));
462
+ if (args.status) params.set('status', String(args.status));
463
+ if (args.assignee) params.set('assignee', String(args.assignee));
464
+ return client.get(`/tasks?${params}`);
465
+ },
466
+ claim_task: (client, args) => client.post(`/tasks/${args.task_id}/claim`),
467
+ complete_task: (client, args) => {
468
+ const { task_id, ...body } = args;
469
+ return client.post(`/tasks/${task_id}/complete`, body);
470
+ },
471
+ block_task: (client, args) => {
472
+ const { task_id, ...body } = args;
473
+ return client.post(`/tasks/${task_id}/block`, body);
474
+ },
475
+
476
+ // Agents
477
+ list_agents: (client, args) => {
478
+ const params = new URLSearchParams();
479
+ if (args.status) params.set('status', String(args.status));
480
+ return client.get(`/agents?${params}`);
481
+ },
482
+ get_agent: (client, args) => client.get(`/agents/${args.agent_id}`),
483
+ create_agent: (client, args) => client.post('/agents', args),
484
+ update_agent: (client, args) => {
485
+ const { agent_id, ...body } = args;
486
+ return client.patch(`/agents/${agent_id}`, body);
487
+ },
488
+
489
+ // Blockers
490
+ list_blockers: (client, args) => {
491
+ const params = new URLSearchParams();
492
+ if (args.artifact_id) params.set('artifact_id', String(args.artifact_id));
493
+ if (args.status) params.set('status', String(args.status));
494
+ return client.get(`/blockers?${params}`);
495
+ },
496
+ create_blocker: (client, args) => {
497
+ const { artifact_id, ...body } = args;
498
+ return client.post(`/artifacts/${artifact_id}/blockers`, body);
499
+ },
500
+ resolve_blocker: (client, args) => {
501
+ const { blocker_id, ...body } = args;
502
+ return client.post(`/blockers/${blocker_id}/resolve`, body);
503
+ },
504
+
505
+ // Search
506
+ search_artifacts: (client, args) => {
507
+ const params = new URLSearchParams();
508
+ params.set('q', String(args.query));
509
+ if (args.type) params.set('type', String(args.type));
510
+ if (args.limit) params.set('limit', String(args.limit));
511
+ return client.get(`/search?${params}`);
512
+ },
513
+
514
+ // Context
515
+ get_task_context: (client, args) => client.get(`/tasks/${args.task_id}/context`),
516
+ };
517
+
518
+ // ============================================================================
519
+ // MCP Server Class
520
+ // ============================================================================
521
+
522
+ export class ArtyfactsMcpServer {
523
+ private server: Server;
524
+ private client: ArtyfactsApiClient;
525
+ private config: McpServerConfig;
526
+
527
+ constructor(config: McpServerConfig) {
528
+ this.config = config;
529
+ this.client = new ArtyfactsApiClient(
530
+ config.baseUrl || 'https://artyfacts.dev/api/v1',
531
+ config.apiKey
532
+ );
533
+
534
+ this.server = new Server(
535
+ {
536
+ name: config.name || 'artyfacts-mcp',
537
+ version: config.version || '1.0.0',
538
+ },
539
+ {
540
+ capabilities: {
541
+ tools: {},
542
+ },
543
+ }
544
+ );
545
+
546
+ this.setupHandlers();
547
+ }
548
+
549
+ private setupHandlers(): void {
550
+ // List available tools
551
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
552
+ return { tools: ARTYFACTS_TOOLS };
553
+ });
554
+
555
+ // Handle tool calls
556
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
557
+ const { name, arguments: args } = request.params;
558
+
559
+ const handler = toolHandlers[name];
560
+ if (!handler) {
561
+ return {
562
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
563
+ isError: true,
564
+ };
565
+ }
566
+
567
+ try {
568
+ const result = await handler(this.client, args || {});
569
+ return {
570
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
571
+ };
572
+ } catch (error) {
573
+ const message = error instanceof Error ? error.message : String(error);
574
+ return {
575
+ content: [{ type: 'text', text: `Error: ${message}` }],
576
+ isError: true,
577
+ };
578
+ }
579
+ });
580
+ }
581
+
582
+ async start(): Promise<void> {
583
+ const transport = new StdioServerTransport();
584
+ await this.server.connect(transport);
585
+
586
+ // Log to stderr so it doesn't interfere with MCP protocol on stdout
587
+ console.error(`Artyfacts MCP server running (${ARTYFACTS_TOOLS.length} tools)`);
588
+ }
589
+
590
+ getServer(): Server {
591
+ return this.server;
592
+ }
593
+ }
594
+
595
+ // ============================================================================
596
+ // Factory Functions
597
+ // ============================================================================
598
+
599
+ export function createMcpServer(config: McpServerConfig): ArtyfactsMcpServer {
600
+ return new ArtyfactsMcpServer(config);
601
+ }
602
+
603
+ export async function startServer(server: ArtyfactsMcpServer): Promise<void> {
604
+ await server.start();
605
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }