@spec-r/mcp-server 0.3.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.
Files changed (3) hide show
  1. package/README.md +128 -0
  2. package/dist/index.js +1637 -0
  3. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,1637 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/api-client.ts
8
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ function assertId(value, label) {
10
+ if (!UUID_RE.test(value)) {
11
+ throw new Error(`Invalid ${label}: must be a UUID`);
12
+ }
13
+ }
14
+
15
+ class SpecRApiClient {
16
+ apiKey;
17
+ baseUrl;
18
+ defaultProjectId;
19
+ constructor(config) {
20
+ const parsed = new URL(config.baseUrl);
21
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
22
+ throw new Error("SPECR_BASE_URL must use http or https");
23
+ }
24
+ this.apiKey = config.apiKey;
25
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
26
+ this.defaultProjectId = config.defaultProjectId;
27
+ }
28
+ async request(path, init) {
29
+ const url = `${this.baseUrl}/api/v1${path}`;
30
+ const controller = new AbortController;
31
+ const timeout = setTimeout(() => controller.abort(), 30000);
32
+ const res = await fetch(url, {
33
+ ...init,
34
+ signal: controller.signal,
35
+ headers: {
36
+ Authorization: `Bearer ${this.apiKey}`,
37
+ "Content-Type": "application/json",
38
+ ...init?.headers
39
+ }
40
+ }).finally(() => clearTimeout(timeout));
41
+ if (!res.ok) {
42
+ const body = await res.text();
43
+ throw new Error(`spec-r API error (${res.status}): ${body}`);
44
+ }
45
+ const contentType = res.headers.get("content-type") || "";
46
+ if (contentType.includes("text/markdown")) {
47
+ return await res.text();
48
+ }
49
+ return res.json();
50
+ }
51
+ async listProjects() {
52
+ return this.request("/projects");
53
+ }
54
+ async createProject(data) {
55
+ return this.request("/projects", {
56
+ method: "POST",
57
+ body: JSON.stringify(data)
58
+ });
59
+ }
60
+ async updateProject(projectId, data) {
61
+ assertId(projectId, "projectId");
62
+ return this.request(`/projects/${projectId}`, {
63
+ method: "PUT",
64
+ body: JSON.stringify(data)
65
+ });
66
+ }
67
+ async listSpecs(projectId) {
68
+ assertId(projectId, "projectId");
69
+ return this.request(`/projects/${projectId}/specs`);
70
+ }
71
+ async getSpec(specId, format = "json") {
72
+ assertId(specId, "specId");
73
+ if (format === "json") {
74
+ return this.request(`/specs/${specId}?format=json`);
75
+ }
76
+ return this.request(`/specs/${specId}?format=${format}`);
77
+ }
78
+ async searchSpecs(query, projectId) {
79
+ const params = new URLSearchParams({ q: query });
80
+ if (projectId)
81
+ params.set("projectId", projectId);
82
+ return this.request(`/search?${params}`);
83
+ }
84
+ async getImplementationStatus(specId) {
85
+ assertId(specId, "specId");
86
+ return this.request(`/specs/${specId}/implementation`);
87
+ }
88
+ async updateImplementationStatus(specId, stepIndex, status, extra) {
89
+ assertId(specId, "specId");
90
+ return this.request(`/specs/${specId}/implementation`, {
91
+ method: "PUT",
92
+ body: JSON.stringify({ stepIndex, status, ...extra })
93
+ });
94
+ }
95
+ async publish(projectId, spec, blocks) {
96
+ assertId(projectId, "projectId");
97
+ return this.request("/publish", {
98
+ method: "POST",
99
+ body: JSON.stringify({ projectId, spec, blocks })
100
+ });
101
+ }
102
+ async createSpec(projectId, data) {
103
+ assertId(projectId, "projectId");
104
+ return this.request(`/projects/${projectId}/specs`, {
105
+ method: "POST",
106
+ body: JSON.stringify(data)
107
+ });
108
+ }
109
+ async addBlock(specId, block) {
110
+ assertId(specId, "specId");
111
+ return this.request(`/specs/${specId}/blocks`, {
112
+ method: "POST",
113
+ body: JSON.stringify(block)
114
+ });
115
+ }
116
+ async updateBlock(specId, blockId, content) {
117
+ assertId(specId, "specId");
118
+ assertId(blockId, "blockId");
119
+ return this.request(`/specs/${specId}/blocks/${blockId}`, {
120
+ method: "PUT",
121
+ body: JSON.stringify({ content })
122
+ });
123
+ }
124
+ async updateSpec(specId, data) {
125
+ assertId(specId, "specId");
126
+ return this.request(`/specs/${specId}`, {
127
+ method: "PUT",
128
+ body: JSON.stringify(data)
129
+ });
130
+ }
131
+ async deleteBlock(specId, blockId) {
132
+ assertId(specId, "specId");
133
+ assertId(blockId, "blockId");
134
+ return this.request(`/specs/${specId}/blocks/${blockId}`, {
135
+ method: "DELETE"
136
+ });
137
+ }
138
+ async analyzeScreenshot(projectId, image, options) {
139
+ assertId(projectId, "projectId");
140
+ return this.request("/analyze-screenshot", {
141
+ method: "POST",
142
+ body: JSON.stringify({
143
+ projectId,
144
+ image,
145
+ additionalContext: options?.additionalContext,
146
+ enableGrouping: options?.enableGrouping ?? false
147
+ })
148
+ });
149
+ }
150
+ async analyzeFeedback(projectId, feedback) {
151
+ assertId(projectId, "projectId");
152
+ return this.request("/analyze-feedback", {
153
+ method: "POST",
154
+ body: JSON.stringify({ projectId, feedback })
155
+ });
156
+ }
157
+ async listResources(projectId, type) {
158
+ assertId(projectId, "projectId");
159
+ const params = type ? `?type=${encodeURIComponent(type)}` : "";
160
+ return this.request(`/projects/${projectId}/resources${params}`);
161
+ }
162
+ async getResource(projectId, resourceId) {
163
+ assertId(projectId, "projectId");
164
+ assertId(resourceId, "resourceId");
165
+ return this.request(`/projects/${projectId}/resources/${resourceId}`);
166
+ }
167
+ async createResource(projectId, data) {
168
+ assertId(projectId, "projectId");
169
+ return this.request(`/projects/${projectId}/resources`, {
170
+ method: "POST",
171
+ body: JSON.stringify(data)
172
+ });
173
+ }
174
+ async updateResource(projectId, resourceId, data) {
175
+ assertId(projectId, "projectId");
176
+ assertId(resourceId, "resourceId");
177
+ return this.request(`/projects/${projectId}/resources/${resourceId}`, {
178
+ method: "PUT",
179
+ body: JSON.stringify(data)
180
+ });
181
+ }
182
+ async deleteResource(projectId, resourceId) {
183
+ assertId(projectId, "projectId");
184
+ assertId(resourceId, "resourceId");
185
+ return this.request(`/projects/${projectId}/resources/${resourceId}`, {
186
+ method: "DELETE"
187
+ });
188
+ }
189
+ }
190
+
191
+ // src/tools/read-tools.ts
192
+ import { z } from "zod";
193
+
194
+ // src/constants.ts
195
+ var CHARACTER_LIMIT = 25000;
196
+ function handleApiError(error) {
197
+ if (error instanceof Error) {
198
+ const msg = error.message;
199
+ if (msg.includes("(404)") || msg.includes("error (404)"))
200
+ return "Error: Resource not found. Check the ID is correct.";
201
+ if (msg.includes("(403)") || msg.includes("error (403)"))
202
+ return "Error: Permission denied.";
203
+ if (msg.includes("(429)") || msg.includes("error (429)"))
204
+ return "Error: Rate limit exceeded. Please wait before retrying.";
205
+ if (msg.includes("abort") || msg.includes("timeout"))
206
+ return "Error: Request timed out. Please try again.";
207
+ return `Error: ${msg}`;
208
+ }
209
+ return `Error: ${String(error)}`;
210
+ }
211
+
212
+ // src/tools/read-tools.ts
213
+ function registerReadTools(server, client) {
214
+ server.registerTool("list_specs", {
215
+ title: "List Specs",
216
+ description: [
217
+ "List all specifications in a project.",
218
+ "",
219
+ "Args:",
220
+ " projectId (optional) — Project UUID. Falls back to the default project configured via SPECR_PROJECT_ID.",
221
+ "",
222
+ "Returns:",
223
+ " A bullet list of specs with title, type, status, and ID.",
224
+ "",
225
+ "Examples:",
226
+ " list_specs() — lists specs in the default project",
227
+ ' list_specs({ projectId: "uuid" }) — lists specs in a specific project'
228
+ ].join(`
229
+ `),
230
+ inputSchema: z.object({
231
+ projectId: z.string().optional().describe("Project ID. Uses default project if not specified.")
232
+ }).strict(),
233
+ annotations: {
234
+ readOnlyHint: true,
235
+ destructiveHint: false,
236
+ idempotentHint: true,
237
+ openWorldHint: false
238
+ }
239
+ }, async ({ projectId }) => {
240
+ const pid = projectId || client.defaultProjectId;
241
+ if (!pid) {
242
+ return {
243
+ content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }]
244
+ };
245
+ }
246
+ try {
247
+ const specs = await client.listSpecs(pid);
248
+ const text = specs.length === 0 ? "No specs found in this project." : specs.map((s) => `- **${s.title}** (${s.type}) [${s.status}] — ID: ${s.id}`).join(`
249
+ `);
250
+ return { content: [{ type: "text", text }] };
251
+ } catch (error) {
252
+ return { content: [{ type: "text", text: handleApiError(error) }] };
253
+ }
254
+ });
255
+ server.registerTool("get_spec", {
256
+ title: "Get Spec",
257
+ description: [
258
+ "Get a full specification in agent-optimized format.",
259
+ "Returns implementation-ready markdown with data models, API endpoints, components, and implementation plan.",
260
+ "",
261
+ "Args:",
262
+ " specId — UUID of the spec to retrieve.",
263
+ ' format (optional, default "agent") — Output format:',
264
+ ' "agent" — optimized markdown for AI implementation (default)',
265
+ ' "markdown" — standard markdown',
266
+ ' "json" — raw JSON with spec metadata and blocks array',
267
+ "",
268
+ "Returns:",
269
+ " The spec content in the requested format.",
270
+ ' When format is "json", also returns structuredContent with the parsed data.',
271
+ "",
272
+ "Examples:",
273
+ ' get_spec({ specId: "uuid" }) — fetch in agent format',
274
+ ' get_spec({ specId: "uuid", format: "json" }) — fetch raw JSON'
275
+ ].join(`
276
+ `),
277
+ inputSchema: z.object({
278
+ specId: z.string().describe("The spec ID to retrieve"),
279
+ format: z.enum(["agent", "markdown", "json"]).optional().default("agent").describe('Output format. "agent" is optimized for AI implementation.')
280
+ }).strict(),
281
+ annotations: {
282
+ readOnlyHint: true,
283
+ destructiveHint: false,
284
+ idempotentHint: true,
285
+ openWorldHint: false
286
+ }
287
+ }, async ({ specId, format }) => {
288
+ try {
289
+ if (format === "json") {
290
+ const data = await client.getSpec(specId, "json");
291
+ const text = JSON.stringify(data, null, 2);
292
+ return {
293
+ content: [{ type: "text", text }],
294
+ structuredContent: data
295
+ };
296
+ }
297
+ const markdown = await client.getSpec(specId, format ?? "agent");
298
+ return { content: [{ type: "text", text: markdown }] };
299
+ } catch (error) {
300
+ return { content: [{ type: "text", text: handleApiError(error) }] };
301
+ }
302
+ });
303
+ server.registerTool("search_specs", {
304
+ title: "Search Specs",
305
+ description: [
306
+ "Search across all specifications and blocks. Finds specs by title or block content.",
307
+ "",
308
+ "Args:",
309
+ " query — Search string.",
310
+ " projectId (optional) — Limit search to a specific project UUID.",
311
+ "",
312
+ "Returns:",
313
+ " Matching specs (title, type, status, ID, project) and matching blocks (type, spec title, spec ID).",
314
+ ` Output is capped at ${CHARACTER_LIMIT} characters.`,
315
+ "",
316
+ "Examples:",
317
+ ' search_specs({ query: "authentication" })',
318
+ ' search_specs({ query: "user", projectId: "uuid" })'
319
+ ].join(`
320
+ `),
321
+ inputSchema: z.object({
322
+ query: z.string().describe("Search query"),
323
+ projectId: z.string().optional().describe("Limit search to a specific project")
324
+ }).strict(),
325
+ annotations: {
326
+ readOnlyHint: true,
327
+ destructiveHint: false,
328
+ idempotentHint: true,
329
+ openWorldHint: false
330
+ }
331
+ }, async ({ query, projectId }) => {
332
+ try {
333
+ const pid = projectId || client.defaultProjectId;
334
+ const { results } = await client.searchSpecs(query, pid);
335
+ const lines = [];
336
+ if (results.specs.length > 0) {
337
+ lines.push("**Matching Specs:**");
338
+ results.specs.forEach((s) => {
339
+ lines.push(`- ${s.specTitle} (${s.specType}) [${s.specStatus}] — ID: ${s.specId} — Project: ${s.projectName}`);
340
+ });
341
+ }
342
+ if (results.blocks.length > 0) {
343
+ lines.push("");
344
+ lines.push("**Matching Blocks:**");
345
+ results.blocks.forEach((b) => {
346
+ lines.push(`- [${b.blockType}] in "${b.specTitle}" — Spec ID: ${b.specId}`);
347
+ });
348
+ }
349
+ if (lines.length === 0) {
350
+ lines.push(`No results found for "${query}"`);
351
+ }
352
+ const text = lines.join(`
353
+ `);
354
+ if (text.length > CHARACTER_LIMIT) {
355
+ return {
356
+ content: [{ type: "text", text: text.slice(0, CHARACTER_LIMIT) + `
357
+
358
+ [Output truncated at character limit]` }]
359
+ };
360
+ }
361
+ return { content: [{ type: "text", text }] };
362
+ } catch (error) {
363
+ return { content: [{ type: "text", text: handleApiError(error) }] };
364
+ }
365
+ });
366
+ server.registerTool("list_projects", {
367
+ title: "List Projects",
368
+ description: [
369
+ "List all projects accessible with the current API key.",
370
+ "Use this to discover project IDs when SPECR_PROJECT_ID is not configured.",
371
+ "",
372
+ "Args:",
373
+ " (none)",
374
+ "",
375
+ "Returns:",
376
+ " A bullet list of projects with name, ID, industry, and business model.",
377
+ "",
378
+ "Examples:",
379
+ " list_projects() — discover available projects and their UUIDs"
380
+ ].join(`
381
+ `),
382
+ inputSchema: z.object({}).strict(),
383
+ annotations: {
384
+ readOnlyHint: true,
385
+ destructiveHint: false,
386
+ idempotentHint: true,
387
+ openWorldHint: false
388
+ }
389
+ }, async () => {
390
+ try {
391
+ const projects = await client.listProjects();
392
+ if (projects.length === 0) {
393
+ return { content: [{ type: "text", text: "No projects found." }] };
394
+ }
395
+ const text = projects.map((p) => {
396
+ const meta = [p.industry, p.businessModel].filter(Boolean).join(", ");
397
+ return `- **${p.name}** — ID: ${p.id}${meta ? ` (${meta})` : ""}`;
398
+ }).join(`
399
+ `);
400
+ return { content: [{ type: "text", text }] };
401
+ } catch (error) {
402
+ return { content: [{ type: "text", text: handleApiError(error) }] };
403
+ }
404
+ });
405
+ server.registerTool("get_project_context", {
406
+ title: "Get Project Context",
407
+ description: [
408
+ "Get project context including tech stack, industry, and conventions.",
409
+ "Useful for understanding the technical environment before implementing specs.",
410
+ "",
411
+ "Args:",
412
+ " projectId (optional) — Project UUID. Falls back to the default project, or the first project.",
413
+ "",
414
+ "Returns:",
415
+ " Project name, description, industry, business model, and tech stack (frontend, backend, database, infra).",
416
+ "",
417
+ "Examples:",
418
+ " get_project_context() — context for the default project",
419
+ ' get_project_context({ projectId: "uuid" })'
420
+ ].join(`
421
+ `),
422
+ inputSchema: z.object({
423
+ projectId: z.string().optional().describe("Project ID. Uses default project if not specified.")
424
+ }).strict(),
425
+ annotations: {
426
+ readOnlyHint: true,
427
+ destructiveHint: false,
428
+ idempotentHint: true,
429
+ openWorldHint: false
430
+ }
431
+ }, async ({ projectId }) => {
432
+ try {
433
+ const projects = await client.listProjects();
434
+ const pid = projectId || client.defaultProjectId;
435
+ const project = pid ? projects.find((p) => p.id === pid) : projects[0];
436
+ if (!project) {
437
+ return { content: [{ type: "text", text: "Project not found" }] };
438
+ }
439
+ const lines = [
440
+ `# ${project.name}`,
441
+ "",
442
+ project.description || "",
443
+ ""
444
+ ];
445
+ if (project.industry)
446
+ lines.push(`- **Industry:** ${project.industry}`);
447
+ if (project.businessModel)
448
+ lines.push(`- **Business Model:** ${project.businessModel}`);
449
+ if (project.techStack) {
450
+ const ts = project.techStack;
451
+ lines.push("");
452
+ lines.push("**Tech Stack:**");
453
+ if (ts.frontend?.length)
454
+ lines.push(`- Frontend: ${ts.frontend.join(", ")}`);
455
+ if (ts.backend?.length)
456
+ lines.push(`- Backend: ${ts.backend.join(", ")}`);
457
+ if (ts.database?.length)
458
+ lines.push(`- Database: ${ts.database.join(", ")}`);
459
+ if (ts.infra?.length)
460
+ lines.push(`- Infra: ${ts.infra.join(", ")}`);
461
+ }
462
+ return { content: [{ type: "text", text: lines.join(`
463
+ `) }] };
464
+ } catch (error) {
465
+ return { content: [{ type: "text", text: handleApiError(error) }] };
466
+ }
467
+ });
468
+ server.registerTool("get_data_models", {
469
+ title: "Get Data Models",
470
+ description: [
471
+ "Get all data models defined across specs in a project.",
472
+ "Returns entity names, fields (name, type, required), and relations.",
473
+ "",
474
+ "Args:",
475
+ " projectId (optional) — Project UUID. Falls back to the default project.",
476
+ "",
477
+ "Returns:",
478
+ " Markdown tables for each data_model block found across all specs.",
479
+ ` Output is capped at ${CHARACTER_LIMIT} characters.`,
480
+ "",
481
+ "Examples:",
482
+ " get_data_models() — all models in the default project",
483
+ ' get_data_models({ projectId: "uuid" })'
484
+ ].join(`
485
+ `),
486
+ inputSchema: z.object({
487
+ projectId: z.string().optional().describe("Project ID. Uses default project if not specified.")
488
+ }).strict(),
489
+ annotations: {
490
+ readOnlyHint: true,
491
+ destructiveHint: false,
492
+ idempotentHint: true,
493
+ openWorldHint: false
494
+ }
495
+ }, async ({ projectId }) => {
496
+ const pid = projectId || client.defaultProjectId;
497
+ if (!pid) {
498
+ return {
499
+ content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }]
500
+ };
501
+ }
502
+ try {
503
+ const specs = await client.listSpecs(pid);
504
+ const specData = await Promise.all(specs.map((spec) => client.getSpec(spec.id, "json").then((data) => ({ spec, data }))));
505
+ const models = ["# Data Models", ""];
506
+ for (const { spec, data } of specData) {
507
+ const dataModelBlocks = data.blocks.filter((b) => b.type === "data_model");
508
+ for (const block of dataModelBlocks) {
509
+ const c = block.content;
510
+ const fields = c.fields || [];
511
+ const relations = c.relations || [];
512
+ models.push(`## ${c.entityName} (from "${spec.title}")`);
513
+ if (c.description)
514
+ models.push(c.description);
515
+ models.push("");
516
+ if (fields.length > 0) {
517
+ models.push("| Field | Type | Required |");
518
+ models.push("|-------|------|----------|");
519
+ fields.forEach((f) => {
520
+ models.push(`| ${f.name} | ${f.type} | ${f.required ? "Yes" : "No"} |`);
521
+ });
522
+ models.push("");
523
+ }
524
+ if (relations.length > 0) {
525
+ models.push("**Relations:**");
526
+ relations.forEach((r) => models.push(`- ${r.type} → ${r.target}`));
527
+ models.push("");
528
+ }
529
+ }
530
+ }
531
+ if (models.length === 2) {
532
+ return { content: [{ type: "text", text: "No data models found in this project." }] };
533
+ }
534
+ const text = models.join(`
535
+ `);
536
+ if (text.length > CHARACTER_LIMIT) {
537
+ return {
538
+ content: [{ type: "text", text: text.slice(0, CHARACTER_LIMIT) + `
539
+
540
+ [Output truncated at character limit]` }]
541
+ };
542
+ }
543
+ return { content: [{ type: "text", text }] };
544
+ } catch (error) {
545
+ return { content: [{ type: "text", text: handleApiError(error) }] };
546
+ }
547
+ });
548
+ }
549
+
550
+ // src/tools/write-tools.ts
551
+ import { z as z2 } from "zod";
552
+ function registerWriteTools(server, client) {
553
+ const techStackSchema = z2.object({
554
+ frontend: z2.array(z2.string()).optional().describe('e.g. ["Next.js", "React"]'),
555
+ mobile: z2.array(z2.string()).optional().describe('e.g. ["React Native", "Expo"]'),
556
+ backend: z2.array(z2.string()).optional().describe('e.g. ["Node.js", "Go"]'),
557
+ database: z2.array(z2.string()).optional().describe('e.g. ["PostgreSQL", "Redis"]'),
558
+ infra: z2.array(z2.string()).optional().describe('e.g. ["Vercel", "AWS"]'),
559
+ auth: z2.array(z2.string()).optional().describe('e.g. ["Email/Password", "OAuth / Social"]'),
560
+ deployment: z2.array(z2.string()).optional(),
561
+ integrations: z2.array(z2.string()).optional().describe('e.g. ["Stripe", "Resend", "PostHog"]')
562
+ });
563
+ const conventionsSchema = z2.object({
564
+ keyFeatures: z2.string().optional().describe("Main features the product needs"),
565
+ targetAudience: z2.string().optional().describe("Who the product is for"),
566
+ competitors: z2.array(z2.string()).optional().describe('Reference products (e.g. ["Notion", "Linear"])'),
567
+ projectPhase: z2.enum(["ideation", "mvp", "scaling", "maintenance"]).optional(),
568
+ teamSize: z2.enum(["solo", "small", "medium", "large"]).optional(),
569
+ timeline: z2.string().optional(),
570
+ constraints: z2.array(z2.string()).optional(),
571
+ codingStyle: z2.string().optional(),
572
+ designSystem: z2.string().optional()
573
+ });
574
+ server.registerTool("create_project", {
575
+ title: "Create Project",
576
+ description: [
577
+ "Create a new spec-r project — equivalent to the web wizard.",
578
+ "Provide as much context as possible: tech stack and conventions help AI generate more relevant specs.",
579
+ "",
580
+ "Args:",
581
+ " name — Project name (required).",
582
+ " description — What it does, who it is for, what problem it solves.",
583
+ ' industry — e.g. "SaaS", "Fintech", "EdTech".',
584
+ " businessModel — B2B | B2C | B2B2C.",
585
+ " techStack — Object with frontend/mobile/backend/database/infra/auth/integrations arrays.",
586
+ " conventions — Object with keyFeatures, targetAudience, competitors, projectPhase, teamSize, etc.",
587
+ "",
588
+ "Returns:",
589
+ " Project ID — save it as SPECR_PROJECT_ID for subsequent spec operations.",
590
+ "",
591
+ "Example:",
592
+ ' create_project({ name: "Lumo", description: "Gamified fragrance tracker",',
593
+ ' industry: "SaaS", businessModel: "B2C",',
594
+ ' techStack: { frontend: ["Next.js"], database: ["PostgreSQL", "Supabase"] },',
595
+ ' conventions: { projectPhase: "mvp", targetAudience: "Fragrance collectors 18-45" } })'
596
+ ].join(`
597
+ `),
598
+ inputSchema: z2.object({
599
+ name: z2.string().max(100).describe("Project name"),
600
+ description: z2.string().max(2000).optional().describe("What it does, who it is for"),
601
+ industry: z2.string().max(100).optional().describe("e.g. SaaS, Fintech, EdTech"),
602
+ businessModel: z2.enum(["B2B", "B2C", "B2B2C"]).optional(),
603
+ techStack: techStackSchema.optional(),
604
+ conventions: conventionsSchema.optional()
605
+ }).strict(),
606
+ annotations: {
607
+ readOnlyHint: false,
608
+ destructiveHint: false,
609
+ idempotentHint: false,
610
+ openWorldHint: false
611
+ }
612
+ }, async ({ name, description, industry, businessModel, techStack, conventions }) => {
613
+ try {
614
+ const project = await client.createProject({ name, description, industry, businessModel, techStack, conventions });
615
+ return {
616
+ content: [{
617
+ type: "text",
618
+ text: [
619
+ `Project "${project.name}" created.`,
620
+ `- Project ID: ${project.id}`,
621
+ `- Set SPECR_PROJECT_ID=${project.id} or pass projectId to spec tools.`
622
+ ].join(`
623
+ `)
624
+ }]
625
+ };
626
+ } catch (error) {
627
+ return { content: [{ type: "text", text: handleApiError(error) }] };
628
+ }
629
+ });
630
+ server.registerTool("update_project", {
631
+ title: "Update Project",
632
+ description: [
633
+ "Update an existing project's metadata: name, description, industry, tech stack, or conventions.",
634
+ "Use this to enrich project context after creation, or to update the tech stack as it evolves.",
635
+ "",
636
+ "Args:",
637
+ " projectId — Project UUID (required).",
638
+ " name — New project name.",
639
+ " description — Updated description.",
640
+ " industry — Updated industry.",
641
+ " businessModel — B2B | B2C | B2B2C.",
642
+ " techStack — Updated tech stack (replaces existing).",
643
+ " conventions — Updated conventions (replaces existing).",
644
+ "",
645
+ "Returns:",
646
+ " Confirmation with project ID."
647
+ ].join(`
648
+ `),
649
+ inputSchema: z2.object({
650
+ projectId: z2.string().describe("Project UUID to update"),
651
+ name: z2.string().max(100).optional(),
652
+ description: z2.string().max(2000).optional(),
653
+ industry: z2.string().max(100).optional(),
654
+ businessModel: z2.enum(["B2B", "B2C", "B2B2C"]).optional(),
655
+ techStack: techStackSchema.optional(),
656
+ conventions: conventionsSchema.optional()
657
+ }).strict(),
658
+ annotations: {
659
+ readOnlyHint: false,
660
+ destructiveHint: false,
661
+ idempotentHint: true,
662
+ openWorldHint: false
663
+ }
664
+ }, async ({ projectId, name, description, industry, businessModel, techStack, conventions }) => {
665
+ try {
666
+ const project = await client.updateProject(projectId, { name, description, industry, businessModel, techStack, conventions });
667
+ return {
668
+ content: [{ type: "text", text: `Project "${project.name}" (${project.id}) updated.` }]
669
+ };
670
+ } catch (error) {
671
+ return { content: [{ type: "text", text: handleApiError(error) }] };
672
+ }
673
+ });
674
+ server.registerTool("publish_spec", {
675
+ title: "Publish Spec",
676
+ description: [
677
+ "Publish a complete spec (create or update by title). This is the primary tool for creating specs — provide all blocks at once.",
678
+ "If a spec with the same title already exists in the project, it will be updated (blocks replaced).",
679
+ "",
680
+ "Read the resource spec-r://block-types first to get the exact content schema for each block type.",
681
+ "",
682
+ "Args:",
683
+ " title — Spec title (used to detect duplicates).",
684
+ " type — Spec type: prd | full | feature | epic | bug-fix.",
685
+ ' status (optional, default "draft") — draft | review | approved.',
686
+ " blocks — Array of block objects, each with { type, content, position }.",
687
+ " projectId (optional) — Project UUID. Falls back to the default project.",
688
+ "",
689
+ "Returns:",
690
+ " Spec ID, action (created/updated), and block count.",
691
+ "",
692
+ "Examples:",
693
+ ' publish_spec({ title: "Auth flow", type: "feature", blocks: [...] })'
694
+ ].join(`
695
+ `),
696
+ inputSchema: z2.object({
697
+ title: z2.string().describe("Spec title"),
698
+ type: z2.enum(["prd", "full", "feature", "epic", "bug-fix"]).describe("Spec type"),
699
+ status: z2.enum(["draft", "review", "approved"]).optional().default("draft").describe("Spec status"),
700
+ blocks: z2.array(z2.object({
701
+ type: z2.string().describe("Block type (heading, text, user_story, api_endpoint, data_model, etc.)"),
702
+ content: z2.record(z2.string(), z2.any()).describe("Block content object matching the block type schema"),
703
+ position: z2.number().describe("Block position (0-based)")
704
+ })).describe("Array of blocks that make up the spec"),
705
+ projectId: z2.string().optional().describe("Project ID. Uses default project if not specified.")
706
+ }).strict(),
707
+ annotations: {
708
+ readOnlyHint: false,
709
+ destructiveHint: false,
710
+ idempotentHint: false,
711
+ openWorldHint: false
712
+ }
713
+ }, async ({ title, type, status, blocks, projectId }) => {
714
+ const pid = projectId || client.defaultProjectId;
715
+ if (!pid) {
716
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
717
+ }
718
+ try {
719
+ const result = await client.publish(pid, { title, type, status }, blocks);
720
+ return {
721
+ content: [
722
+ {
723
+ type: "text",
724
+ text: `Spec "${title}" ${result.action} successfully.
725
+ - Spec ID: ${result.specId}
726
+ - Blocks: ${result.blocksCount}`
727
+ }
728
+ ]
729
+ };
730
+ } catch (error) {
731
+ return { content: [{ type: "text", text: handleApiError(error) }] };
732
+ }
733
+ });
734
+ server.registerTool("create_spec", {
735
+ title: "Create Spec",
736
+ description: [
737
+ "Create a new empty spec (without blocks).",
738
+ "Use add_block to add blocks afterwards, or use publish_spec to create a spec with blocks in one step.",
739
+ "",
740
+ "Args:",
741
+ " title — Spec title.",
742
+ " type — Spec type: prd | full | feature | epic | bug-fix.",
743
+ ' status (optional, default "draft") — draft | review | approved.',
744
+ " projectId (optional) — Project UUID. Falls back to the default project.",
745
+ "",
746
+ "Returns:",
747
+ " Spec ID, type, and status.",
748
+ "",
749
+ "Examples:",
750
+ ' create_spec({ title: "Payments", type: "feature" })'
751
+ ].join(`
752
+ `),
753
+ inputSchema: z2.object({
754
+ title: z2.string().describe("Spec title"),
755
+ type: z2.enum(["prd", "full", "feature", "epic", "bug-fix"]).describe("Spec type"),
756
+ status: z2.enum(["draft", "review", "approved"]).optional().default("draft"),
757
+ projectId: z2.string().optional().describe("Project ID. Uses default project if not specified.")
758
+ }).strict(),
759
+ annotations: {
760
+ readOnlyHint: false,
761
+ destructiveHint: false,
762
+ idempotentHint: false,
763
+ openWorldHint: false
764
+ }
765
+ }, async ({ title, type, status, projectId }) => {
766
+ const pid = projectId || client.defaultProjectId;
767
+ if (!pid) {
768
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
769
+ }
770
+ try {
771
+ const spec = await client.createSpec(pid, { title, type, status });
772
+ return {
773
+ content: [{ type: "text", text: `Spec "${title}" created.
774
+ - Spec ID: ${spec.id}
775
+ - Type: ${spec.type}
776
+ - Status: ${spec.status}` }]
777
+ };
778
+ } catch (error) {
779
+ return { content: [{ type: "text", text: handleApiError(error) }] };
780
+ }
781
+ });
782
+ server.registerTool("add_block", {
783
+ title: "Add Block",
784
+ description: [
785
+ "Add a single block to an existing spec.",
786
+ "If position is omitted, the block is appended at the end.",
787
+ "",
788
+ "Read the resource spec-r://block-types first to get the exact content schema for each block type.",
789
+ "",
790
+ "Args:",
791
+ " specId — UUID of the spec to add the block to.",
792
+ " type — Block type: heading | text | user_story | api_endpoint | data_model | component_spec | etc.",
793
+ " content — Block content object matching the block type schema.",
794
+ " position (optional) — 0-based position. Appends at end if omitted.",
795
+ "",
796
+ "Returns:",
797
+ " Block ID, type, and final position.",
798
+ "",
799
+ "Examples:",
800
+ ' add_block({ specId: "uuid", type: "text", content: { text: "..." } })'
801
+ ].join(`
802
+ `),
803
+ inputSchema: z2.object({
804
+ specId: z2.string().describe("The spec ID to add the block to"),
805
+ type: z2.string().describe("Block type (heading, text, user_story, api_endpoint, data_model, component_spec, etc.)"),
806
+ content: z2.record(z2.string(), z2.any()).describe("Block content object matching the block type schema"),
807
+ position: z2.number().optional().describe("Position in the spec (0-based). Appends at end if omitted.")
808
+ }).strict(),
809
+ annotations: {
810
+ readOnlyHint: false,
811
+ destructiveHint: false,
812
+ idempotentHint: false,
813
+ openWorldHint: false
814
+ }
815
+ }, async ({ specId, type: blockType, content, position }) => {
816
+ try {
817
+ const block = await client.addBlock(specId, { type: blockType, content, position });
818
+ return {
819
+ content: [{ type: "text", text: `Block added.
820
+ - Block ID: ${block.id}
821
+ - Type: ${block.type}
822
+ - Position: ${block.position}` }]
823
+ };
824
+ } catch (error) {
825
+ return { content: [{ type: "text", text: handleApiError(error) }] };
826
+ }
827
+ });
828
+ server.registerTool("update_block", {
829
+ title: "Update Block",
830
+ description: [
831
+ "Update the content of an existing block. Provide the full content object (not a partial patch).",
832
+ "",
833
+ "Args:",
834
+ " specId — UUID of the spec containing the block.",
835
+ " blockId — UUID of the block to update.",
836
+ " content — New block content (full replacement, not a diff).",
837
+ "",
838
+ "Returns:",
839
+ " Confirmation with the updated block ID.",
840
+ "",
841
+ "Examples:",
842
+ ' update_block({ specId: "uuid", blockId: "uuid", content: { text: "updated..." } })'
843
+ ].join(`
844
+ `),
845
+ inputSchema: z2.object({
846
+ specId: z2.string().describe("The spec ID containing the block"),
847
+ blockId: z2.string().describe("The block ID to update"),
848
+ content: z2.record(z2.string(), z2.any()).describe("New block content (full replacement)")
849
+ }).strict(),
850
+ annotations: {
851
+ readOnlyHint: false,
852
+ destructiveHint: true,
853
+ idempotentHint: true,
854
+ openWorldHint: false
855
+ }
856
+ }, async ({ specId, blockId, content }) => {
857
+ try {
858
+ const block = await client.updateBlock(specId, blockId, content);
859
+ return {
860
+ content: [{ type: "text", text: `Block ${block.id} updated successfully.` }]
861
+ };
862
+ } catch (error) {
863
+ return { content: [{ type: "text", text: handleApiError(error) }] };
864
+ }
865
+ });
866
+ server.registerTool("delete_block", {
867
+ title: "Delete Block",
868
+ description: [
869
+ "Delete a block from a spec. This is irreversible.",
870
+ "Prefer update_block if you want to modify content rather than remove the block entirely.",
871
+ "",
872
+ "Args:",
873
+ " specId — UUID of the spec containing the block.",
874
+ " blockId — UUID of the block to delete.",
875
+ "",
876
+ "Returns:",
877
+ " Confirmation of deletion.",
878
+ "",
879
+ "Examples:",
880
+ ' delete_block({ specId: "uuid", blockId: "uuid" })'
881
+ ].join(`
882
+ `),
883
+ inputSchema: z2.object({
884
+ specId: z2.string().describe("The spec ID containing the block"),
885
+ blockId: z2.string().describe("The block ID to delete")
886
+ }).strict(),
887
+ annotations: {
888
+ readOnlyHint: false,
889
+ destructiveHint: true,
890
+ idempotentHint: false,
891
+ openWorldHint: false
892
+ }
893
+ }, async ({ specId, blockId }) => {
894
+ try {
895
+ await client.deleteBlock(specId, blockId);
896
+ return { content: [{ type: "text", text: `Block ${blockId} deleted.` }] };
897
+ } catch (error) {
898
+ return { content: [{ type: "text", text: handleApiError(error) }] };
899
+ }
900
+ });
901
+ server.registerTool("update_spec", {
902
+ title: "Update Spec",
903
+ description: [
904
+ "Update spec metadata: title, type, or status. At least one field must be provided.",
905
+ "",
906
+ "Args:",
907
+ " specId — UUID of the spec to update.",
908
+ " title (optional) — New title.",
909
+ " type (optional) — New type: prd | full | feature | epic | bug-fix.",
910
+ " status (optional) — New status: draft | review | approved | locked.",
911
+ "",
912
+ "Returns:",
913
+ " Confirmation with updated fields.",
914
+ "",
915
+ "Examples:",
916
+ ' update_spec({ specId: "uuid", status: "approved" })',
917
+ ' update_spec({ specId: "uuid", title: "New title", type: "prd" })'
918
+ ].join(`
919
+ `),
920
+ inputSchema: z2.object({
921
+ specId: z2.string().describe("The spec ID to update"),
922
+ title: z2.string().optional().describe("New title"),
923
+ type: z2.enum(["prd", "full", "feature", "epic", "bug-fix"]).optional().describe("New type"),
924
+ status: z2.enum(["draft", "review", "approved", "locked"]).optional().describe("New status")
925
+ }).strict(),
926
+ annotations: {
927
+ readOnlyHint: false,
928
+ destructiveHint: false,
929
+ idempotentHint: true,
930
+ openWorldHint: false
931
+ }
932
+ }, async ({ specId, title, type: specType, status }) => {
933
+ try {
934
+ const data = {};
935
+ if (title)
936
+ data.title = title;
937
+ if (specType)
938
+ data.type = specType;
939
+ if (status)
940
+ data.status = status;
941
+ await client.updateSpec(specId, data);
942
+ return {
943
+ content: [{ type: "text", text: `Spec ${specId} updated.${title ? ` Title: "${title}"` : ""}${status ? ` Status: ${status}` : ""}` }]
944
+ };
945
+ } catch (error) {
946
+ return { content: [{ type: "text", text: handleApiError(error) }] };
947
+ }
948
+ });
949
+ }
950
+
951
+ // src/tools/status-tools.ts
952
+ import { z as z3 } from "zod";
953
+ function registerStatusTools(server, client) {
954
+ server.registerTool("get_implementation_status", {
955
+ title: "Get Implementation Status",
956
+ description: [
957
+ "Get the implementation tracking status for a spec.",
958
+ "Shows progress percentage and per-step status (pending / in_progress / done / blocked).",
959
+ "Call this before starting work on a spec to know what is already done.",
960
+ "",
961
+ "Args:",
962
+ " specId — UUID of the spec.",
963
+ "",
964
+ "Returns:",
965
+ " Overall progress (0–100%), list of steps with status, commit SHA, PR URL, and notes.",
966
+ "",
967
+ "Examples:",
968
+ ' get_implementation_status({ specId: "uuid" })'
969
+ ].join(`
970
+ `),
971
+ inputSchema: z3.object({
972
+ specId: z3.string().describe("The spec ID")
973
+ }).strict(),
974
+ annotations: {
975
+ readOnlyHint: true,
976
+ destructiveHint: false,
977
+ idempotentHint: true,
978
+ openWorldHint: false
979
+ }
980
+ }, async ({ specId }) => {
981
+ try {
982
+ const { progress, steps } = await client.getImplementationStatus(specId);
983
+ if (steps.length === 0) {
984
+ return { content: [{ type: "text", text: "No implementation steps tracked yet for this spec." }] };
985
+ }
986
+ const lines = [`**Progress: ${progress}%**`, ""];
987
+ steps.forEach((step, i) => {
988
+ const statusIcon = {
989
+ done: "✓",
990
+ in_progress: "▶",
991
+ blocked: "✗",
992
+ pending: "○"
993
+ };
994
+ const icon = statusIcon[step.status] ?? "○";
995
+ lines.push(`${icon} Step ${i + 1}: ${step.stepTitle} [${step.status}]`);
996
+ if (step.commitSha)
997
+ lines.push(` commit: ${step.commitSha}`);
998
+ if (step.prUrl)
999
+ lines.push(` PR: ${step.prUrl}`);
1000
+ if (step.notes)
1001
+ lines.push(` notes: ${step.notes}`);
1002
+ });
1003
+ return { content: [{ type: "text", text: lines.join(`
1004
+ `) }] };
1005
+ } catch (error) {
1006
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1007
+ }
1008
+ });
1009
+ server.registerTool("update_implementation_status", {
1010
+ title: "Update Implementation Status",
1011
+ description: [
1012
+ "Update the implementation status of a spec step. Use this to track progress as you implement features.",
1013
+ "",
1014
+ "Args:",
1015
+ " specId — UUID of the spec.",
1016
+ " stepIndex — 0-based index of the step to update.",
1017
+ " status — New status: pending | in_progress | done | blocked.",
1018
+ " commitSha (optional) — Git commit SHA associated with this step.",
1019
+ " prUrl (optional) — Pull request URL.",
1020
+ " notes (optional) — Implementation notes.",
1021
+ "",
1022
+ "Returns:",
1023
+ " Confirmation with the step number and new status.",
1024
+ "",
1025
+ "Examples:",
1026
+ ' update_implementation_status({ specId: "uuid", stepIndex: 0, status: "done", commitSha: "abc123" })',
1027
+ ' update_implementation_status({ specId: "uuid", stepIndex: 2, status: "blocked", notes: "Waiting on design" })'
1028
+ ].join(`
1029
+ `),
1030
+ inputSchema: z3.object({
1031
+ specId: z3.string().describe("The spec ID"),
1032
+ stepIndex: z3.number().describe("The step index (0-based)"),
1033
+ status: z3.enum(["pending", "in_progress", "done", "blocked"]).describe("New status for this step"),
1034
+ commitSha: z3.string().optional().describe("Git commit SHA (if applicable)"),
1035
+ prUrl: z3.string().optional().describe("Pull request URL (if applicable)"),
1036
+ notes: z3.string().optional().describe("Implementation notes")
1037
+ }).strict(),
1038
+ annotations: {
1039
+ readOnlyHint: false,
1040
+ destructiveHint: false,
1041
+ idempotentHint: true,
1042
+ openWorldHint: false
1043
+ }
1044
+ }, async ({ specId, stepIndex, status, commitSha, prUrl, notes }) => {
1045
+ try {
1046
+ await client.updateImplementationStatus(specId, stepIndex, status, {
1047
+ commitSha,
1048
+ prUrl,
1049
+ notes
1050
+ });
1051
+ const statusLabel = {
1052
+ pending: "pending",
1053
+ in_progress: "in progress",
1054
+ done: "done",
1055
+ blocked: "blocked"
1056
+ };
1057
+ return {
1058
+ content: [
1059
+ {
1060
+ type: "text",
1061
+ text: `Step ${stepIndex + 1} marked as ${statusLabel[status] ?? status}${commitSha ? ` (commit: ${commitSha})` : ""}`
1062
+ }
1063
+ ]
1064
+ };
1065
+ } catch (error) {
1066
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1067
+ }
1068
+ });
1069
+ }
1070
+
1071
+ // src/tools/ai-tools.ts
1072
+ import { z as z4 } from "zod";
1073
+ function registerAiTools(server, client) {
1074
+ server.registerTool("analyze_screenshot", {
1075
+ title: "Analyze Screenshot",
1076
+ description: [
1077
+ "Analyze a wireframe or mockup screenshot and extract structured specs: screens, user stories, data models, and API endpoints.",
1078
+ "Use publish_spec to save the results.",
1079
+ "Pass enableGrouping=true to get suggested spec splits for complex UIs.",
1080
+ "",
1081
+ "Args:",
1082
+ " image — Base64-encoded image of the wireframe or mockup.",
1083
+ " projectId (optional) — Project UUID. Falls back to the default project.",
1084
+ ' additionalContext (optional) — Extra context to guide the analysis (e.g. "mobile app", "admin dashboard").',
1085
+ " enableGrouping (optional, default false) — If true, also suggests how to split the analysis into multiple focused specs.",
1086
+ "",
1087
+ "Returns:",
1088
+ " JSON with screens, userStories, dataModels, apiEndpoints.",
1089
+ " When enableGrouping=true, also includes suggestedSpecs and suggestedRelations.",
1090
+ "",
1091
+ "Examples:",
1092
+ ' analyze_screenshot({ image: "<base64>", additionalContext: "mobile checkout flow" })',
1093
+ ' analyze_screenshot({ image: "<base64>", enableGrouping: true })'
1094
+ ].join(`
1095
+ `),
1096
+ inputSchema: z4.object({
1097
+ image: z4.string().max(1e7).describe("Base64-encoded image of the wireframe or mockup (max ~7.5 MB)"),
1098
+ projectId: z4.string().optional().describe("Project ID. Uses default project if not specified."),
1099
+ additionalContext: z4.string().max(2000).optional().describe('Extra context to guide the analysis (e.g. "mobile app", "admin dashboard")'),
1100
+ enableGrouping: z4.boolean().optional().default(false).describe("If true, also suggests how to split the analysis into multiple focused specs")
1101
+ }).strict(),
1102
+ annotations: {
1103
+ readOnlyHint: false,
1104
+ destructiveHint: false,
1105
+ idempotentHint: false,
1106
+ openWorldHint: false
1107
+ }
1108
+ }, async ({ image, projectId, additionalContext, enableGrouping }) => {
1109
+ const pid = projectId || client.defaultProjectId;
1110
+ if (!pid) {
1111
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1112
+ }
1113
+ try {
1114
+ const analysis = await client.analyzeScreenshot(pid, image, { additionalContext, enableGrouping });
1115
+ return { content: [{ type: "text", text: JSON.stringify(analysis, null, 2) }] };
1116
+ } catch (error) {
1117
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1118
+ }
1119
+ });
1120
+ server.registerTool("analyze_feedback", {
1121
+ title: "Analyze Feedback",
1122
+ description: [
1123
+ "Analyze client feedback text and extract structured feature requests, bug reports, and improvements with priorities.",
1124
+ "Returns suggested spec blocks ready to use with publish_spec.",
1125
+ "",
1126
+ "Args:",
1127
+ " feedback — Raw client feedback text to analyze.",
1128
+ " projectId (optional) — Project UUID. Falls back to the default project.",
1129
+ "",
1130
+ "Returns:",
1131
+ " Summary, list of requests (type, priority, title, description, suggestedBlocks), and full JSON.",
1132
+ "",
1133
+ "Examples:",
1134
+ ' analyze_feedback({ feedback: "The login page is confusing and we need 2FA..." })'
1135
+ ].join(`
1136
+ `),
1137
+ inputSchema: z4.object({
1138
+ feedback: z4.string().max(50000).describe("Raw client feedback text to analyze (max 50 000 chars)"),
1139
+ projectId: z4.string().optional().describe("Project ID. Uses default project if not specified.")
1140
+ }).strict(),
1141
+ annotations: {
1142
+ readOnlyHint: false,
1143
+ destructiveHint: false,
1144
+ idempotentHint: false,
1145
+ openWorldHint: false
1146
+ }
1147
+ }, async ({ feedback, projectId }) => {
1148
+ const pid = projectId || client.defaultProjectId;
1149
+ if (!pid) {
1150
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1151
+ }
1152
+ try {
1153
+ const analysis = await client.analyzeFeedback(pid, feedback);
1154
+ const lines = [
1155
+ `**Summary:** ${analysis.summary}`,
1156
+ "",
1157
+ `**${analysis.requests.length} request(s) identified:**`,
1158
+ ""
1159
+ ];
1160
+ analysis.requests.forEach((req, i) => {
1161
+ lines.push(`### ${i + 1}. [${req.type.toUpperCase()}] ${req.title} — Priority: ${req.priority}`);
1162
+ lines.push(req.description);
1163
+ lines.push(`_${req.suggestedBlocks.length} suggested block(s) ready for publish_spec_`);
1164
+ lines.push("");
1165
+ });
1166
+ lines.push("---");
1167
+ lines.push("Full JSON:");
1168
+ lines.push("```json");
1169
+ lines.push(JSON.stringify(analysis, null, 2));
1170
+ lines.push("```");
1171
+ return { content: [{ type: "text", text: lines.join(`
1172
+ `) }] };
1173
+ } catch (error) {
1174
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1175
+ }
1176
+ });
1177
+ }
1178
+
1179
+ // src/resources/index.ts
1180
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1181
+ var BLOCK_TYPE_DOCS = {
1182
+ heading: {
1183
+ description: "Section heading.",
1184
+ required: ["level (1|2|3|4)", "text"],
1185
+ optional: [],
1186
+ example: { level: 2, text: "Overview" }
1187
+ },
1188
+ text: {
1189
+ description: "Free-form markdown text.",
1190
+ required: ["text"],
1191
+ optional: [],
1192
+ example: { text: "This spec covers..." }
1193
+ },
1194
+ user_story: {
1195
+ description: "User story: actor / action / goal + acceptance criteria.",
1196
+ required: ["id (e.g. US-001)", "actor", "action", "goal", "priority (HIGH|MEDIUM|LOW)", "acceptanceCriteria (string[])"],
1197
+ optional: ["storyPoints", "status (todo|in_progress|done)"],
1198
+ example: { id: "US-001", actor: "En tant qu'utilisateur", action: "Je veux me connecter", goal: "Afin d'accéder à mon compte", priority: "HIGH", acceptanceCriteria: ["Token JWT retourné", "Session persistée 30j"] }
1199
+ },
1200
+ acceptance_criteria: {
1201
+ description: "Checklist of acceptance criteria.",
1202
+ required: ["criteria (Array<{id, description, verified}>)"],
1203
+ optional: ["title"],
1204
+ example: { title: "Login", criteria: [{ id: "AC-1", description: "Token valide retourné", verified: false }] }
1205
+ },
1206
+ business_rule: {
1207
+ description: "Business rule with trigger, conditions, and actions.",
1208
+ required: ["id (e.g. BR-001)", "name", "description", "trigger", "conditions (string[])", "actions (string[])"],
1209
+ optional: ["exceptions (string[])", "priority (HIGH|MEDIUM|LOW)"],
1210
+ example: { id: "BR-001", name: "Free tier limit", description: "Max 20 items for free users", trigger: "User adds item", conditions: ["plan === free", "count >= 20"], actions: ["Block action", "Show upgrade modal"] }
1211
+ },
1212
+ persona: {
1213
+ description: "User persona with goals and frustrations.",
1214
+ required: ["name", "role", "description", "goals (string[])", "frustrations (string[])"],
1215
+ optional: ["context"],
1216
+ example: { name: "Marie", role: "Beginner", description: "22 ans, découvre les parfums", goals: ["Apprendre les familles olfactives"], frustrations: ["Jargon trop technique"] }
1217
+ },
1218
+ screen_spec: {
1219
+ description: "UI screen specification with states and components.",
1220
+ required: ["name", "description", "states (Array<{name,description}>)", "components (string[])"],
1221
+ optional: ["route", "interactions (string[])", "screenshot (URL)"],
1222
+ example: { name: "Home", route: "/home", description: "Main screen", states: [{ name: "empty", description: "Nothing logged today" }], components: ["SOTDCard", "StreakBadge"] }
1223
+ },
1224
+ component_spec: {
1225
+ description: "Reusable UI component with props and states.",
1226
+ required: ["name", "filePath", "description", "props (Array<{name,type,required,description?,defaultValue?}>)", "states (string[])", "behavior"],
1227
+ optional: ["events (string[])"],
1228
+ example: { name: "StreakBadge", filePath: "components/StreakBadge.tsx", description: "Shows streak count", props: [{ name: "count", type: "number", required: true }], states: ["active", "broken"], behavior: "Pulses on increment" }
1229
+ },
1230
+ api_endpoint: {
1231
+ description: "API endpoint specification.",
1232
+ required: ["method (GET|POST|PUT|DELETE|PATCH)", "path", "description", "authRequired (boolean)"],
1233
+ optional: ["request ({body?,params?,query?})", "response"],
1234
+ example: { method: "POST", path: "/api/sotd", description: "Log today's fragrance", authRequired: true, request: { body: { fragranceId: "uuid" } }, response: { xpEarned: 10, streakCount: 5 } }
1235
+ },
1236
+ data_model: {
1237
+ description: "Data entity with fields and relations.",
1238
+ required: ["entityName", "description", "fields (Array<{name,type,required,description?}>)", "relations (Array<{type:one_to_one|one_to_many|many_to_many, target}>)"],
1239
+ optional: [],
1240
+ example: { entityName: "DailyLog", description: "Fragrance worn on a given day", fields: [{ name: "id", type: "uuid", required: true }, { name: "date", type: "date", required: true }], relations: [{ type: "many_to_many", target: "Fragrance" }] }
1241
+ },
1242
+ dto: {
1243
+ description: "Data Transfer Object for API validation.",
1244
+ required: ["name", "type (request|response|query|params|event)", "fields (Array<{name,type,required,description?,validation?}>)"],
1245
+ optional: ["description", 'endpointRef (e.g. "POST /api/sotd")', "example (JSON string)"],
1246
+ example: { name: "CreateSOTDRequest", type: "request", endpointRef: "POST /api/sotd", fields: [{ name: "fragranceId", type: "string", required: true, validation: "uuid" }] }
1247
+ },
1248
+ enum_definition: {
1249
+ description: "Enumeration of possible values.",
1250
+ required: ["name", "values (Array<{key,label,description?,color?}>)"],
1251
+ optional: ["description"],
1252
+ example: { name: "FragranceFamily", values: [{ key: "FLORAL", label: "Floral", color: "#FFB3C6" }, { key: "WOODY", label: "Woody", color: "#8B6914" }] }
1253
+ },
1254
+ external_dependency: {
1255
+ description: "External API, service, or SDK dependency.",
1256
+ required: ["name", "type (api|webhook|database|service|sdk)", "description"],
1257
+ optional: ["baseUrl", "endpoints (Array<{method,path,description}>)", "auth", "rateLimit", "dataFormat"],
1258
+ example: { name: "Stripe", type: "api", baseUrl: "https://api.stripe.com", description: "Payment processing", auth: "Bearer secret key" }
1259
+ },
1260
+ problem_statement: {
1261
+ description: "The problem being solved, its impact, and target audience.",
1262
+ required: ["problem", "impact", "audience"],
1263
+ optional: [],
1264
+ example: { problem: "No dedicated app for fragrance tracking", impact: "Behavior exists on Reddit but is fragmented", audience: "Fragrance collectors 18-45" }
1265
+ },
1266
+ solution_overview: {
1267
+ description: "Proposed solution with positioning and success metrics.",
1268
+ required: ["solution", "positioning", "successMetrics (string[])"],
1269
+ optional: [],
1270
+ example: { solution: "Gamified fragrance tracking app", positioning: "Duolingo for perfume", successMetrics: ["DAU/MAU > 40%", "Pro conversion > 5%"] }
1271
+ },
1272
+ scope_boundary: {
1273
+ description: "What is in scope, out of scope, and deferred.",
1274
+ required: ["inScope (string[])", "outOfScope (string[])", "deferredItems (string[])"],
1275
+ optional: [],
1276
+ example: { inScope: ["SOTD logging", "Collection"], outOfScope: ["Social network"], deferredItems: ["AI recognition"] }
1277
+ },
1278
+ diagram: {
1279
+ description: "Mermaid diagram (flowchart, sequence, ERD, class).",
1280
+ required: ["diagramType (flowchart|sequence|erd|class)", "mermaidCode"],
1281
+ optional: [],
1282
+ example: { diagramType: "sequence", mermaidCode: `sequenceDiagram
1283
+ User->>API: POST /api/sotd
1284
+ API-->>User: {xpEarned}` }
1285
+ },
1286
+ code: {
1287
+ description: "Code snippet with syntax highlighting.",
1288
+ required: ["language (e.g. typescript, sql)", "code"],
1289
+ optional: [],
1290
+ example: { language: "typescript", code: "const streak = await getStreak(userId)" }
1291
+ },
1292
+ image: {
1293
+ description: "Image with optional caption.",
1294
+ required: ["url (https)"],
1295
+ optional: ["alt", "caption"],
1296
+ example: { url: "https://example.com/wireframe.png", caption: "Home screen wireframe" }
1297
+ },
1298
+ spec_link: {
1299
+ description: "Link to another spec with a relation type.",
1300
+ required: ["targetSpecId (UUID)", "relationType (depends_on|related_to|parent_of|blocks)"],
1301
+ optional: ["targetSpecTitle", "description"],
1302
+ example: { targetSpecId: "uuid", relationType: "depends_on", description: "Requires gamification XP system" }
1303
+ },
1304
+ file_structure: {
1305
+ description: "Files to create, modify, or delete.",
1306
+ required: ["description", "entries (Array<{path, action:create|modify|delete, description}>)"],
1307
+ optional: [],
1308
+ example: { description: "SOTD feature files", entries: [{ path: "app/api/sotd/route.ts", action: "create", description: "SOTD API endpoint" }] }
1309
+ },
1310
+ architecture_decision: {
1311
+ description: "Architecture Decision Record (ADR).",
1312
+ required: ["title", "status (proposed|accepted|deprecated|superseded)", "context", "decision", "consequences (string[])"],
1313
+ optional: ["alternatives (Array<{option, reason}>)"],
1314
+ example: { title: "Edge caching with Cloudflare", status: "accepted", context: "Read-heavy catalog", decision: "Cache at edge via KV", consequences: ["<50ms globally", "Cache invalidation complexity"] }
1315
+ },
1316
+ test_spec: {
1317
+ description: "Test scenarios in Given/When/Then format.",
1318
+ required: ["testType (unit|integration|e2e|api)", "description", "scenarios (Array<{given,when,then,priority?}>)"],
1319
+ optional: [],
1320
+ example: { testType: "api", description: "SOTD endpoint", scenarios: [{ given: "User authenticated", when: "POST /api/sotd", then: "Returns 201 + xpEarned", priority: "HIGH" }] }
1321
+ },
1322
+ implementation_plan: {
1323
+ description: "Ordered implementation steps with files and complexity.",
1324
+ required: ["steps (Array<{order,title,description,files:string[],complexity:low|medium|high,dependencies?}>)"],
1325
+ optional: [],
1326
+ example: { steps: [{ order: 1, title: "DB schema", description: "Create table", files: ["schema/daily-logs.ts"], complexity: "low" }, { order: 2, title: "API endpoint", description: "POST handler", files: ["app/api/sotd/route.ts"], complexity: "medium", dependencies: ["Step 1"] }] }
1327
+ },
1328
+ environment_config: {
1329
+ description: "Environment variables and services required.",
1330
+ required: ["description", "variables (Array<{name,description,required,example?,secret}>)"],
1331
+ optional: ["services (Array<{name,description,required}>)"],
1332
+ example: { description: "Affiliate config", variables: [{ name: "FRAGRANCENET_API_KEY", description: "Affiliate API key", required: true, secret: true, example: "fn_xxx" }] }
1333
+ }
1334
+ };
1335
+ function registerResources(server, client) {
1336
+ server.registerResource("block-types", "spec-r://block-types", {
1337
+ description: "Complete schema reference for all spec-r block types. Read this before calling add_block or publish_spec to know what fields each block type requires.",
1338
+ mimeType: "application/json"
1339
+ }, async (uri) => ({
1340
+ contents: [{
1341
+ uri: uri.toString(),
1342
+ mimeType: "application/json",
1343
+ text: JSON.stringify(BLOCK_TYPE_DOCS, null, 2)
1344
+ }]
1345
+ }));
1346
+ server.registerResource("project-specs", new ResourceTemplate("spec-r://projects/{projectId}/specs", { list: undefined }), {
1347
+ description: "List of all specs in a project. Use to browse specs without calling list_specs.",
1348
+ mimeType: "application/json"
1349
+ }, async (uri, { projectId }) => {
1350
+ try {
1351
+ const specs = await client.listSpecs(projectId);
1352
+ return {
1353
+ contents: [{
1354
+ uri: uri.toString(),
1355
+ mimeType: "application/json",
1356
+ text: JSON.stringify(specs, null, 2)
1357
+ }]
1358
+ };
1359
+ } catch (error) {
1360
+ return {
1361
+ contents: [{
1362
+ uri: uri.toString(),
1363
+ mimeType: "text/plain",
1364
+ text: handleApiError(error)
1365
+ }]
1366
+ };
1367
+ }
1368
+ });
1369
+ server.registerResource("project-resources", new ResourceTemplate("spec-r://projects/{projectId}/resources", { list: undefined }), {
1370
+ description: "Shared resources (data_model, persona, enum_definition, external_dependency) defined at project level. Read before creating specs to avoid duplicating definitions.",
1371
+ mimeType: "application/json"
1372
+ }, async (uri, { projectId }) => {
1373
+ try {
1374
+ const { resources } = await client.listResources(projectId);
1375
+ return {
1376
+ contents: [{
1377
+ uri: uri.toString(),
1378
+ mimeType: "application/json",
1379
+ text: JSON.stringify(resources, null, 2)
1380
+ }]
1381
+ };
1382
+ } catch (error) {
1383
+ return {
1384
+ contents: [{ uri: uri.toString(), mimeType: "text/plain", text: handleApiError(error) }]
1385
+ };
1386
+ }
1387
+ });
1388
+ server.registerResource("spec", new ResourceTemplate("spec-r://specs/{specId}", { list: undefined }), {
1389
+ description: "Full content of a spec including all blocks. Alternative to get_spec tool for passive reading.",
1390
+ mimeType: "application/json"
1391
+ }, async (uri, { specId }) => {
1392
+ try {
1393
+ const data = await client.getSpec(specId, "json");
1394
+ return {
1395
+ contents: [{
1396
+ uri: uri.toString(),
1397
+ mimeType: "application/json",
1398
+ text: JSON.stringify(data, null, 2)
1399
+ }]
1400
+ };
1401
+ } catch (error) {
1402
+ return {
1403
+ contents: [{
1404
+ uri: uri.toString(),
1405
+ mimeType: "text/plain",
1406
+ text: handleApiError(error)
1407
+ }]
1408
+ };
1409
+ }
1410
+ });
1411
+ }
1412
+
1413
+ // src/tools/resource-tools.ts
1414
+ import { z as z5 } from "zod";
1415
+ var RESOURCE_TYPES = ["data_model", "persona", "enum_definition", "external_dependency"];
1416
+ function registerResourceTools(server, client) {
1417
+ server.registerTool("list_resources", {
1418
+ title: "List Shared Resources",
1419
+ description: [
1420
+ "List shared resources defined at the project level.",
1421
+ "Shared resources (data_model, persona, enum_definition, external_dependency) are reusable",
1422
+ "entities that can be referenced from any spec block via linkedResourceId.",
1423
+ "Always check shared resources before defining new data models or personas in a spec —",
1424
+ "the project may already have a canonical definition to link to.",
1425
+ "",
1426
+ "Args:",
1427
+ " projectId (optional) — Project UUID. Falls back to default project.",
1428
+ " type (optional) — Filter by type: data_model | persona | enum_definition | external_dependency",
1429
+ "",
1430
+ "Returns:",
1431
+ " List of resources with id, type, name, description."
1432
+ ].join(`
1433
+ `),
1434
+ inputSchema: z5.object({
1435
+ projectId: z5.string().optional().describe("Project ID. Uses default project if not specified."),
1436
+ type: z5.enum(RESOURCE_TYPES).optional().describe("Filter by resource type")
1437
+ }).strict(),
1438
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1439
+ }, async ({ projectId, type }) => {
1440
+ const pid = projectId || client.defaultProjectId;
1441
+ if (!pid)
1442
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1443
+ try {
1444
+ const { resources } = await client.listResources(pid, type);
1445
+ if (resources.length === 0) {
1446
+ return { content: [{ type: "text", text: `No shared resources found${type ? ` of type "${type}"` : ""}.` }] };
1447
+ }
1448
+ const text = resources.map((r) => `- [${r.type}] **${r.name}** — ID: ${r.id}${r.description ? ` — ${r.description}` : ""}`).join(`
1449
+ `);
1450
+ return { content: [{ type: "text", text }] };
1451
+ } catch (error) {
1452
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1453
+ }
1454
+ });
1455
+ server.registerTool("get_resource", {
1456
+ title: "Get Shared Resource",
1457
+ description: [
1458
+ "Get a shared resource by ID, including its full content and which specs reference it.",
1459
+ "",
1460
+ "Args:",
1461
+ " projectId (optional) — Project UUID.",
1462
+ " resourceId — UUID of the resource.",
1463
+ "",
1464
+ "Returns:",
1465
+ " Resource content + usedIn list of {specId, blockId} where it is referenced."
1466
+ ].join(`
1467
+ `),
1468
+ inputSchema: z5.object({
1469
+ projectId: z5.string().optional().describe("Project ID. Uses default project if not specified."),
1470
+ resourceId: z5.string().describe("Resource UUID")
1471
+ }).strict(),
1472
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1473
+ }, async ({ projectId, resourceId }) => {
1474
+ const pid = projectId || client.defaultProjectId;
1475
+ if (!pid)
1476
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1477
+ try {
1478
+ const { resource, usedIn } = await client.getResource(pid, resourceId);
1479
+ const lines = [
1480
+ `# [${resource.type}] ${resource.name}`,
1481
+ resource.description ? `> ${resource.description}` : "",
1482
+ "",
1483
+ "**Content:**",
1484
+ "```json",
1485
+ JSON.stringify(resource.content, null, 2),
1486
+ "```",
1487
+ "",
1488
+ `**Used in ${usedIn.length} spec block(s):**`,
1489
+ ...usedIn.map((u) => `- Spec: ${u.specId} / Block: ${u.blockId}`)
1490
+ ].filter((l) => l !== undefined);
1491
+ const text = lines.join(`
1492
+ `);
1493
+ return {
1494
+ content: [{ type: "text", text: text.length > CHARACTER_LIMIT ? text.slice(0, CHARACTER_LIMIT) + `
1495
+ …(truncated)` : text }],
1496
+ structuredContent: { resource, usedIn }
1497
+ };
1498
+ } catch (error) {
1499
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1500
+ }
1501
+ });
1502
+ server.registerTool("create_resource", {
1503
+ title: "Create Shared Resource",
1504
+ description: [
1505
+ "Create a shared resource at the project level.",
1506
+ "Use this instead of repeating the same data_model or persona definition in every spec.",
1507
+ "Once created, reference it from spec blocks via linkedResourceId in the block content.",
1508
+ "",
1509
+ "Types and their content schema (see spec-r://block-types for full field details):",
1510
+ " data_model — entityName, description, fields[], relations[]",
1511
+ " persona — name, role, description, goals[], frustrations[]",
1512
+ " enum_definition — name, values[{key,label,description?,color?}]",
1513
+ " external_dependency — name, type, description, baseUrl?, endpoints?[]",
1514
+ "",
1515
+ "Args:",
1516
+ " type — data_model | persona | enum_definition | external_dependency",
1517
+ " name — Human-readable name for the resource",
1518
+ " content — Type-specific content object (same schema as the matching block type)",
1519
+ " description (optional) — Short description",
1520
+ " projectId (optional) — Falls back to default project",
1521
+ "",
1522
+ "Returns:",
1523
+ " Created resource with its UUID to use as linkedResourceId."
1524
+ ].join(`
1525
+ `),
1526
+ inputSchema: z5.object({
1527
+ projectId: z5.string().optional().describe("Project ID. Uses default project if not specified."),
1528
+ type: z5.enum(RESOURCE_TYPES).describe("Resource type"),
1529
+ name: z5.string().min(1).max(200).describe("Resource name"),
1530
+ description: z5.string().max(500).optional().describe("Short description"),
1531
+ content: z5.record(z5.string(), z5.any()).describe("Type-specific content (same schema as the matching block type)")
1532
+ }).strict(),
1533
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1534
+ }, async ({ projectId, type, name, description, content }) => {
1535
+ const pid = projectId || client.defaultProjectId;
1536
+ if (!pid)
1537
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1538
+ try {
1539
+ const resource = await client.createResource(pid, { type, name, description, content });
1540
+ return {
1541
+ content: [{ type: "text", text: `Shared resource created.
1542
+ - ID: ${resource.id}
1543
+ - Type: ${resource.type}
1544
+ - Name: ${resource.name}
1545
+
1546
+ Reference it from spec blocks with: { linkedResourceId: "${resource.id}" }` }],
1547
+ structuredContent: resource
1548
+ };
1549
+ } catch (error) {
1550
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1551
+ }
1552
+ });
1553
+ server.registerTool("update_resource", {
1554
+ title: "Update Shared Resource",
1555
+ description: [
1556
+ "Update a shared resource. All specs referencing it via linkedResourceId will reflect the change.",
1557
+ "",
1558
+ "Args:",
1559
+ " projectId (optional)",
1560
+ " resourceId — UUID of the resource to update",
1561
+ " name (optional) — New name",
1562
+ " description (optional) — New description",
1563
+ " content (optional) — Full content replacement"
1564
+ ].join(`
1565
+ `),
1566
+ inputSchema: z5.object({
1567
+ projectId: z5.string().optional().describe("Project ID. Uses default project if not specified."),
1568
+ resourceId: z5.string().describe("Resource UUID"),
1569
+ name: z5.string().min(1).max(200).optional().describe("New name"),
1570
+ description: z5.string().max(500).optional().describe("New description"),
1571
+ content: z5.record(z5.string(), z5.any()).optional().describe("New content (full replacement)")
1572
+ }).strict(),
1573
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1574
+ }, async ({ projectId, resourceId, name, description, content }) => {
1575
+ const pid = projectId || client.defaultProjectId;
1576
+ if (!pid)
1577
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1578
+ try {
1579
+ const resource = await client.updateResource(pid, resourceId, { name, description, content });
1580
+ return { content: [{ type: "text", text: `Resource ${resource.id} updated.` }] };
1581
+ } catch (error) {
1582
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1583
+ }
1584
+ });
1585
+ server.registerTool("delete_resource", {
1586
+ title: "Delete Shared Resource",
1587
+ description: [
1588
+ "Delete a shared resource. Call get_resource first to check usedIn — deleting a resource",
1589
+ "referenced by spec blocks will leave those blocks with a dangling linkedResourceId.",
1590
+ "",
1591
+ "Args:",
1592
+ " projectId (optional)",
1593
+ " resourceId — UUID of the resource to delete"
1594
+ ].join(`
1595
+ `),
1596
+ inputSchema: z5.object({
1597
+ projectId: z5.string().optional().describe("Project ID. Uses default project if not specified."),
1598
+ resourceId: z5.string().describe("Resource UUID")
1599
+ }).strict(),
1600
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
1601
+ }, async ({ projectId, resourceId }) => {
1602
+ const pid = projectId || client.defaultProjectId;
1603
+ if (!pid)
1604
+ return { content: [{ type: "text", text: "Error: projectId is required (no default project configured)" }] };
1605
+ try {
1606
+ await client.deleteResource(pid, resourceId);
1607
+ return { content: [{ type: "text", text: `Resource ${resourceId} deleted.` }] };
1608
+ } catch (error) {
1609
+ return { content: [{ type: "text", text: handleApiError(error) }] };
1610
+ }
1611
+ });
1612
+ }
1613
+
1614
+ // src/index.ts
1615
+ var apiKey = process.env.SPECR_API_KEY;
1616
+ var baseUrl = process.env.SPECR_BASE_URL || "https://spec-r.app";
1617
+ var defaultProjectId = process.env.SPECR_PROJECT_ID;
1618
+ if (!apiKey) {
1619
+ console.error("Error: SPECR_API_KEY environment variable is required");
1620
+ process.exit(1);
1621
+ }
1622
+ var client = new SpecRApiClient({ apiKey, baseUrl, defaultProjectId });
1623
+ var server = new McpServer2({ name: "spec-r", version: "0.1.0" });
1624
+ registerReadTools(server, client);
1625
+ registerWriteTools(server, client);
1626
+ registerStatusTools(server, client);
1627
+ registerAiTools(server, client);
1628
+ registerResources(server, client);
1629
+ registerResourceTools(server, client);
1630
+ async function main() {
1631
+ const transport = new StdioServerTransport;
1632
+ await server.connect(transport);
1633
+ }
1634
+ main().catch((error) => {
1635
+ console.error("MCP server error:", error);
1636
+ process.exit(1);
1637
+ });