@nimara-app/mcp 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1096 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // ../convex/convex/_generated/api.js
10
+ import { anyApi, componentsGeneric } from "convex/server";
11
+ var api = anyApi;
12
+ var components = componentsGeneric();
13
+
14
+ // src/convex.ts
15
+ import { ConvexHttpClient } from "convex/browser";
16
+ var cachedClient = null;
17
+ function getConvexClient() {
18
+ if (cachedClient) return cachedClient;
19
+ const url = process.env.NIMARA_CONVEX_URL;
20
+ if (!url) {
21
+ throw new Error(
22
+ "NIMARA_CONVEX_URL is not set. Add it to your MCP client config (e.g. Claude Desktop's mcpServers env block)."
23
+ );
24
+ }
25
+ cachedClient = new ConvexHttpClient(url);
26
+ return cachedClient;
27
+ }
28
+ function getToken() {
29
+ const token = process.env.NIMARA_TOKEN;
30
+ if (!token) {
31
+ throw new Error(
32
+ "NIMARA_TOKEN is not set. Generate a personal access token from Nimara \u2192 Settings \u2192 Personal Access Tokens, then add it to your MCP client config."
33
+ );
34
+ }
35
+ return token;
36
+ }
37
+
38
+ // src/tools/list-orgs.ts
39
+ function registerListOrgs(server) {
40
+ server.registerTool(
41
+ "list_orgs",
42
+ {
43
+ description: "List all organizations (and the personal workspace) the authenticated user belongs to. Use this first to discover org IDs needed by other tools.",
44
+ inputSchema: {},
45
+ annotations: {
46
+ readOnlyHint: true,
47
+ openWorldHint: false
48
+ }
49
+ },
50
+ async () => {
51
+ const client = getConvexClient();
52
+ const orgs = await client.action(api.mcp.listOrgs, { token: getToken() });
53
+ return {
54
+ content: [{ type: "text", text: JSON.stringify(orgs, null, 2) }]
55
+ };
56
+ }
57
+ );
58
+ }
59
+
60
+ // src/tools/list-projects.ts
61
+ import { z } from "zod";
62
+ function registerListProjects(server) {
63
+ server.registerTool(
64
+ "list_projects",
65
+ {
66
+ description: "List projects in a given organization. Call list_orgs first to find the orgId.",
67
+ inputSchema: { orgId: z.string().describe("orgId from list_orgs") },
68
+ annotations: {
69
+ readOnlyHint: true,
70
+ openWorldHint: false
71
+ }
72
+ },
73
+ async ({ orgId }) => {
74
+ const client = getConvexClient();
75
+ const projects = await client.action(api.mcp.listProjects, {
76
+ token: getToken(),
77
+ orgId
78
+ });
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify(projects, null, 2) }]
81
+ };
82
+ }
83
+ );
84
+ }
85
+
86
+ // src/tools/list-work-items.ts
87
+ import { z as z2 } from "zod";
88
+ function registerListWorkItems(server) {
89
+ server.registerTool(
90
+ "list_work_items",
91
+ {
92
+ // The description is the only place a model learns paging exists, so it
93
+ // states the shape of the response and what to do when hasMore is true.
94
+ description: "List non-archived work items in a project, newest-created last. Returns { items, nextCursor, hasMore }. Long descriptions are previewed \u2014 an item with descriptionTruncated: true has more text, readable via fullDescriptions. When hasMore is true, call again with cursor set to nextCursor to get the next page. Call list_projects first for the projectId.",
95
+ inputSchema: {
96
+ projectId: z2.string().describe("projectId from list_projects"),
97
+ limit: z2.number().int().min(1).max(200).optional().describe("Items per page (default 50, max 200)."),
98
+ cursor: z2.number().optional().describe(
99
+ "nextCursor from the previous page. Omit for the first page."
100
+ ),
101
+ fullDescriptions: z2.boolean().optional().describe(
102
+ "Return descriptions in full instead of previewed. Costly on large projects \u2014 prefer paging, or read one item at a time."
103
+ )
104
+ },
105
+ annotations: {
106
+ readOnlyHint: true,
107
+ openWorldHint: false
108
+ }
109
+ },
110
+ async ({ projectId, limit, cursor, fullDescriptions }) => {
111
+ const page = await getConvexClient().action(api.mcp.listWorkItems, {
112
+ token: getToken(),
113
+ projectId,
114
+ limit,
115
+ cursor,
116
+ fullDescriptions
117
+ });
118
+ return {
119
+ content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
120
+ };
121
+ }
122
+ );
123
+ }
124
+
125
+ // src/tools/list-ai-review-queue.ts
126
+ import { z as z3 } from "zod";
127
+ function registerListAiReviewQueue(server) {
128
+ server.registerTool(
129
+ "list_ai_review_queue",
130
+ {
131
+ description: "List work items in a project that a human has flagged for AI review (aiReviewRequested). Descriptions come back in full here, since enrichment needs the whole text. Returns { items, nextCursor, hasMore } \u2014 when hasMore is true, call again with cursor set to nextCursor. Process each item, then clear its flag by calling update_work_item with aiReviewRequested: false. Call list_projects first to find the projectId.",
132
+ inputSchema: {
133
+ projectId: z3.string().describe("projectId from list_projects"),
134
+ limit: z3.number().int().min(1).max(200).optional().describe("Items per page (default 50, max 200)."),
135
+ cursor: z3.number().optional().describe(
136
+ "nextCursor from the previous page. Omit for the first page."
137
+ )
138
+ },
139
+ annotations: {
140
+ readOnlyHint: true,
141
+ openWorldHint: false
142
+ }
143
+ },
144
+ async ({ projectId, limit, cursor }) => {
145
+ const page = await getConvexClient().action(api.mcp.listAiReviewQueue, {
146
+ token: getToken(),
147
+ projectId,
148
+ limit,
149
+ cursor
150
+ });
151
+ return {
152
+ content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
153
+ };
154
+ }
155
+ );
156
+ }
157
+
158
+ // src/tools/create-work-item.ts
159
+ import { z as z4 } from "zod";
160
+ function registerCreateWorkItem(server) {
161
+ server.registerTool(
162
+ "create_work_item",
163
+ {
164
+ description: "Create a new work item in a project. Hierarchy rules: epics live at the root; features go under epics; tasks go under epics or features (or root); subtasks must go under a task. Returns the new workItemId and its display ID (e.g. NIM-42).",
165
+ inputSchema: {
166
+ projectId: z4.string().describe("projectId from list_projects"),
167
+ type: z4.enum(["epic", "feature", "task", "subtask"]).describe("Item type \u2014 drives hierarchy validation"),
168
+ title: z4.string().min(1).describe("Short, descriptive title"),
169
+ description: z4.string().optional().describe("Optional markdown body"),
170
+ parentId: z4.string().optional().describe(
171
+ "workItemId of the parent. Required for subtask, optional otherwise."
172
+ ),
173
+ status: z4.string().optional().describe(
174
+ "Status name from the project's statusOrder. Defaults to first status (e.g. 'Backlog')."
175
+ ),
176
+ priority: z4.enum(["p0", "p1", "p2", "p3"]).optional().describe("p0 highest, p3 lowest. Defaults to p2.")
177
+ },
178
+ annotations: {
179
+ readOnlyHint: false,
180
+ destructiveHint: false,
181
+ idempotentHint: false,
182
+ openWorldHint: false
183
+ }
184
+ },
185
+ async (args) => {
186
+ const client = getConvexClient();
187
+ const result = await client.action(api.mcp.createWorkItem, {
188
+ token: getToken(),
189
+ ...args
190
+ });
191
+ return {
192
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
193
+ };
194
+ }
195
+ );
196
+ }
197
+
198
+ // src/tools/create-project.ts
199
+ import { z as z5 } from "zod";
200
+ function registerCreateProject(server) {
201
+ server.registerTool(
202
+ "create_project",
203
+ {
204
+ description: "Create a new project in an org. Prefix must be unique within the org and is uppercased (e.g. 'NIM' yields work items NIM-1, NIM-2\u2026). Caller becomes the project admin. Returns the new projectId.",
205
+ inputSchema: {
206
+ orgId: z5.string().describe("orgId from list_orgs"),
207
+ name: z5.string().min(1).describe("Project name"),
208
+ prefix: z5.string().min(1).max(8).describe(
209
+ "Short uppercase prefix for work item IDs (e.g. 'NIM'). Unique per org."
210
+ ),
211
+ description: z5.string().optional().describe("Optional markdown description"),
212
+ isPrivate: z5.boolean().optional().describe(
213
+ "If true, only explicitly added members see it. Defaults to false."
214
+ ),
215
+ workflowTemplate: z5.enum(["default", "simple", "kanban", "bug"]).optional().describe(
216
+ "Status workflow. Defaults to 'default' (Backlog/Todo/In Progress/Review/Done)."
217
+ )
218
+ },
219
+ annotations: {
220
+ readOnlyHint: false,
221
+ destructiveHint: false,
222
+ idempotentHint: false,
223
+ openWorldHint: false
224
+ }
225
+ },
226
+ async (args) => {
227
+ const client = getConvexClient();
228
+ const result = await client.action(api.mcp.createProject, {
229
+ token: getToken(),
230
+ ...args
231
+ });
232
+ return {
233
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
234
+ };
235
+ }
236
+ );
237
+ }
238
+
239
+ // src/tools/update-work-item.ts
240
+ import { z as z6 } from "zod";
241
+ function registerUpdateWorkItem(server) {
242
+ server.registerTool(
243
+ "update_work_item",
244
+ {
245
+ description: "Update an existing work item: title, description, status, priority, parent, or review flags. Any update also stamps the item as AI-touched.",
246
+ inputSchema: {
247
+ workItemId: z6.string().describe("workItemId from list_work_items"),
248
+ title: z6.string().min(1).optional(),
249
+ description: z6.string().optional(),
250
+ status: z6.string().optional(),
251
+ priority: z6.enum(["p0", "p1", "p2", "p3"]).optional(),
252
+ parentId: z6.string().nullable().optional().describe("New parent workItemId, null to move to root."),
253
+ needsReview: z6.boolean().optional().describe(
254
+ "Flag the item for manual human review \u2014 set true when your enrichment was thin or uncertain."
255
+ ),
256
+ reviewReason: z6.string().optional().describe(
257
+ "Short reason the item needs manual review (shown to the human)."
258
+ ),
259
+ aiReviewRequested: z6.boolean().optional().describe(
260
+ "The human\u2192AI review flag. Set false to clear it after you have reviewed/enriched an item from list_ai_review_queue."
261
+ )
262
+ },
263
+ annotations: {
264
+ readOnlyHint: false,
265
+ destructiveHint: true,
266
+ idempotentHint: true,
267
+ openWorldHint: false
268
+ }
269
+ },
270
+ async (args) => {
271
+ const result = await getConvexClient().action(api.mcp.updateWorkItem, {
272
+ token: getToken(),
273
+ ...args
274
+ });
275
+ return {
276
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
277
+ };
278
+ }
279
+ );
280
+ }
281
+
282
+ // src/tools/create-document.ts
283
+ import { z as z7 } from "zod";
284
+ function registerCreateDocument(server) {
285
+ server.registerTool(
286
+ "create_document",
287
+ {
288
+ description: "Create a markdown project document. Use type 'prd' for project docs or 'fd' for feature/work-item documents.",
289
+ inputSchema: {
290
+ projectId: z7.string().describe("projectId from list_projects"),
291
+ title: z7.string().min(1),
292
+ type: z7.enum(["prd", "fd"]).describe(
293
+ "Document type. 'fd' is stored as the app's feature document type."
294
+ ),
295
+ workItemId: z7.string().optional().describe("Required for fd documents; omitted for prd."),
296
+ content: z7.string().optional().describe("Optional initial markdown content.")
297
+ },
298
+ annotations: {
299
+ readOnlyHint: false,
300
+ destructiveHint: false,
301
+ idempotentHint: false,
302
+ openWorldHint: false
303
+ }
304
+ },
305
+ async (args) => {
306
+ const result = await getConvexClient().action(api.mcp.createDocument, {
307
+ token: getToken(),
308
+ ...args
309
+ });
310
+ return {
311
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
312
+ };
313
+ }
314
+ );
315
+ }
316
+
317
+ // src/tools/update-document.ts
318
+ import { z as z8 } from "zod";
319
+ function registerUpdateDocument(server) {
320
+ server.registerTool(
321
+ "update_document",
322
+ {
323
+ description: "Update an existing document's markdown content and/or title. Use this to fill in or revise a document created earlier (e.g. backfill content into an empty doc). documentId comes from list_documents. Provided content fully replaces the existing content.",
324
+ inputSchema: {
325
+ documentId: z8.string().describe("documentId from list_documents"),
326
+ content: z8.string().optional().describe(
327
+ "New markdown content. Replaces the document's existing content."
328
+ ),
329
+ title: z8.string().min(1).optional().describe("New title (optional).")
330
+ },
331
+ annotations: {
332
+ readOnlyHint: false,
333
+ destructiveHint: true,
334
+ idempotentHint: true,
335
+ openWorldHint: false
336
+ }
337
+ },
338
+ async (args) => {
339
+ const result = await getConvexClient().action(api.mcp.updateDocument, {
340
+ token: getToken(),
341
+ ...args
342
+ });
343
+ return {
344
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
345
+ };
346
+ }
347
+ );
348
+ }
349
+
350
+ // src/tools/list-documents.ts
351
+ import { z as z9 } from "zod";
352
+ function registerListDocuments(server) {
353
+ server.registerTool(
354
+ "list_documents",
355
+ {
356
+ description: "List PRD/FD markdown documents for a project, optionally filtered to one work item.",
357
+ inputSchema: {
358
+ projectId: z9.string().describe("projectId from list_projects"),
359
+ workItemId: z9.string().optional().describe("Optional workItemId to list linked FD documents.")
360
+ },
361
+ annotations: {
362
+ readOnlyHint: true,
363
+ openWorldHint: false
364
+ }
365
+ },
366
+ async (args) => {
367
+ const result = await getConvexClient().action(api.mcp.listDocuments, {
368
+ token: getToken(),
369
+ ...args
370
+ });
371
+ return {
372
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
373
+ };
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/tools/create-project-link.ts
379
+ import { z as z10 } from "zod";
380
+ var projectLinkCategory = z10.enum([
381
+ "api",
382
+ "dashboard",
383
+ "documentation",
384
+ "repository",
385
+ "service",
386
+ "other"
387
+ ]);
388
+ function registerCreateProjectLink(server) {
389
+ server.registerTool(
390
+ "create_project_link",
391
+ {
392
+ description: "Save a link on a project for resources like APIs, dashboards, documentation, repositories, or services.",
393
+ inputSchema: {
394
+ projectId: z10.string().describe("projectId from list_projects"),
395
+ title: z10.string().min(1).max(120),
396
+ url: z10.string().url().describe("http(s) URL to save"),
397
+ description: z10.string().max(500).optional(),
398
+ category: projectLinkCategory.optional().describe("Defaults to other.")
399
+ },
400
+ annotations: {
401
+ readOnlyHint: false,
402
+ destructiveHint: false,
403
+ idempotentHint: false,
404
+ openWorldHint: false
405
+ }
406
+ },
407
+ async (args) => {
408
+ const result = await getConvexClient().action(api.mcp.createProjectLink, {
409
+ token: getToken(),
410
+ ...args
411
+ });
412
+ return {
413
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
414
+ };
415
+ }
416
+ );
417
+ }
418
+
419
+ // src/tools/list-project-links.ts
420
+ import { z as z11 } from "zod";
421
+ function registerListProjectLinks(server) {
422
+ server.registerTool(
423
+ "list_project_links",
424
+ {
425
+ description: "List saved links for a project, such as APIs, dashboards, docs, repositories, and services.",
426
+ inputSchema: {
427
+ projectId: z11.string().describe("projectId from list_projects")
428
+ },
429
+ annotations: {
430
+ readOnlyHint: true,
431
+ openWorldHint: false
432
+ }
433
+ },
434
+ async ({ projectId }) => {
435
+ const result = await getConvexClient().action(api.mcp.listProjectLinks, {
436
+ token: getToken(),
437
+ projectId
438
+ });
439
+ return {
440
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
441
+ };
442
+ }
443
+ );
444
+ }
445
+
446
+ // src/tools/add-work-item-image.ts
447
+ import { z as z12 } from "zod";
448
+ function registerAddWorkItemImage(server) {
449
+ server.registerTool(
450
+ "add_work_item_image",
451
+ {
452
+ description: "Attach an externally hosted image URL to a work item. Use this when an MCP client has a screenshot, mockup, or generated image URL to preserve with the task.",
453
+ inputSchema: {
454
+ workItemId: z12.string().describe("workItemId from list_work_items"),
455
+ imageUrl: z12.string().url().describe("Public or otherwise fetchable http(s) image URL"),
456
+ filename: z12.string().optional().describe("Optional display filename"),
457
+ altText: z12.string().optional().describe("Optional description/caption for the image")
458
+ },
459
+ annotations: {
460
+ readOnlyHint: false,
461
+ destructiveHint: false,
462
+ idempotentHint: false,
463
+ openWorldHint: false
464
+ }
465
+ },
466
+ async (args) => {
467
+ const result = await getConvexClient().action(api.mcp.addWorkItemImage, {
468
+ token: getToken(),
469
+ ...args
470
+ });
471
+ return {
472
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
473
+ };
474
+ }
475
+ );
476
+ }
477
+
478
+ // src/tools/list-work-item-images.ts
479
+ import { z as z13 } from "zod";
480
+ function registerListWorkItemImages(server) {
481
+ server.registerTool(
482
+ "list_work_item_images",
483
+ {
484
+ description: "List image attachments for a work item, including uploaded images and externally attached MCP images. Call list_work_items first to find the workItemId.",
485
+ inputSchema: {
486
+ workItemId: z13.string().describe("workItemId from list_work_items")
487
+ },
488
+ annotations: {
489
+ readOnlyHint: true,
490
+ openWorldHint: false
491
+ }
492
+ },
493
+ async ({ workItemId }) => {
494
+ const result = await getConvexClient().action(
495
+ api.mcp.listWorkItemImages,
496
+ {
497
+ token: getToken(),
498
+ workItemId
499
+ }
500
+ );
501
+ return {
502
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
503
+ };
504
+ }
505
+ );
506
+ }
507
+
508
+ // src/tools/add-work-item-comment.ts
509
+ import { z as z14 } from "zod";
510
+ function registerAddWorkItemComment(server) {
511
+ server.registerTool(
512
+ "add_work_item_comment",
513
+ {
514
+ description: "Add a timestamped comment to a work item. Use this to record notes, updates, or an 'AI touched' entry instead of editing the description. Comments are stamped with the current time automatically and never overwrite each other.",
515
+ inputSchema: {
516
+ workItemId: z14.string().describe("workItemId from list_work_items"),
517
+ body: z14.string().min(1).describe("Comment text (markdown supported)"),
518
+ source: z14.enum(["user", "ai"]).optional().describe('Who authored it; defaults to "ai" for MCP callers')
519
+ },
520
+ annotations: {
521
+ readOnlyHint: false,
522
+ destructiveHint: false,
523
+ idempotentHint: false,
524
+ openWorldHint: false
525
+ }
526
+ },
527
+ async (args) => {
528
+ const result = await getConvexClient().action(
529
+ api.mcp.addWorkItemComment,
530
+ {
531
+ token: getToken(),
532
+ ...args
533
+ }
534
+ );
535
+ return {
536
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
537
+ };
538
+ }
539
+ );
540
+ }
541
+
542
+ // src/tools/list-work-item-comments.ts
543
+ import { z as z15 } from "zod";
544
+ function registerListWorkItemComments(server) {
545
+ server.registerTool(
546
+ "list_work_item_comments",
547
+ {
548
+ description: "List the timestamped comments on a work item, oldest first. Use this to check whether the item was already touched/commented before adding a new comment.",
549
+ inputSchema: {
550
+ workItemId: z15.string().describe("workItemId from list_work_items")
551
+ },
552
+ annotations: {
553
+ readOnlyHint: true,
554
+ openWorldHint: false
555
+ }
556
+ },
557
+ async (args) => {
558
+ const result = await getConvexClient().action(
559
+ api.mcp.listWorkItemComments,
560
+ {
561
+ token: getToken(),
562
+ ...args
563
+ }
564
+ );
565
+ return {
566
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
567
+ };
568
+ }
569
+ );
570
+ }
571
+
572
+ // src/tools/list-validations.ts
573
+ import { z as z16 } from "zod";
574
+ function registerListValidations(server) {
575
+ server.registerTool(
576
+ "list_validations",
577
+ {
578
+ description: "List a project's validation graph: core systems and validation/test tasks with their DERIVED state (passing, failing, stale, unvalidated). A task is 'stale' when a system it covers changed since it was last validated, or an upstream dependency is no longer passing. Call list_projects first to find the projectId.",
579
+ inputSchema: {
580
+ projectId: z16.string().describe("projectId from list_projects")
581
+ },
582
+ annotations: {
583
+ readOnlyHint: true,
584
+ openWorldHint: false
585
+ }
586
+ },
587
+ async ({ projectId }) => {
588
+ const client = getConvexClient();
589
+ const graph = await client.action(api.mcp.listValidations, {
590
+ token: getToken(),
591
+ projectId
592
+ });
593
+ return {
594
+ content: [{ type: "text", text: JSON.stringify(graph, null, 2) }]
595
+ };
596
+ }
597
+ );
598
+ }
599
+
600
+ // src/tools/record-validation.ts
601
+ import { z as z17 } from "zod";
602
+ function registerRecordValidation(server) {
603
+ server.registerTool(
604
+ "record_validation",
605
+ {
606
+ description: "Check off a validation task by recording a pass or fail. Stamps the task as validated 'now', clearing any stale flag until a covered system changes or an upstream dependency moves again. Get validationTaskId from list_validations.",
607
+ inputSchema: {
608
+ validationTaskId: z17.string().describe("validationTaskId from list_validations"),
609
+ result: z17.enum(["pass", "fail"]).describe("Outcome of running the test")
610
+ },
611
+ annotations: {
612
+ readOnlyHint: false,
613
+ destructiveHint: true,
614
+ idempotentHint: true,
615
+ openWorldHint: false
616
+ }
617
+ },
618
+ async ({ validationTaskId, result }) => {
619
+ const client = getConvexClient();
620
+ const res = await client.action(api.mcp.recordValidation, {
621
+ token: getToken(),
622
+ validationTaskId,
623
+ result
624
+ });
625
+ return {
626
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
627
+ };
628
+ }
629
+ );
630
+ }
631
+
632
+ // src/tools/mark-system-changed.ts
633
+ import { z as z18 } from "zod";
634
+ function registerMarkSystemChanged(server) {
635
+ server.registerTool(
636
+ "mark_system_changed",
637
+ {
638
+ description: "Mark a core system as changed. This is the single action that invalidates testing: every validation task that covers this system (and everything transitively depending on those tasks) immediately becomes 'stale' and needs revalidation. Get systemId from list_validations.",
639
+ inputSchema: {
640
+ systemId: z18.string().describe("systemId from list_validations")
641
+ },
642
+ annotations: {
643
+ readOnlyHint: false,
644
+ destructiveHint: false,
645
+ idempotentHint: false,
646
+ openWorldHint: false
647
+ }
648
+ },
649
+ async ({ systemId }) => {
650
+ const client = getConvexClient();
651
+ const res = await client.action(api.mcp.markSystemChanged, {
652
+ token: getToken(),
653
+ systemId
654
+ });
655
+ return {
656
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
657
+ };
658
+ }
659
+ );
660
+ }
661
+
662
+ // src/tools/create-system.ts
663
+ import { z as z19 } from "zod";
664
+ function registerCreateSystem(server) {
665
+ server.registerTool(
666
+ "create_system",
667
+ {
668
+ description: "Create a core system in a project \u2014 a subsystem (e.g. Auth, Payments, Sync) whose change should invalidate the validation tasks that cover it. Returns the new systemId.",
669
+ inputSchema: {
670
+ projectId: z19.string().describe("projectId from list_projects"),
671
+ name: z19.string().min(1).describe("Short system name, e.g. 'Auth'"),
672
+ description: z19.string().optional().describe("Optional details")
673
+ },
674
+ annotations: {
675
+ readOnlyHint: false,
676
+ destructiveHint: false,
677
+ idempotentHint: false,
678
+ openWorldHint: false
679
+ }
680
+ },
681
+ async (args) => {
682
+ const client = getConvexClient();
683
+ const res = await client.action(api.mcp.createSystem, {
684
+ token: getToken(),
685
+ ...args
686
+ });
687
+ return {
688
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
689
+ };
690
+ }
691
+ );
692
+ }
693
+
694
+ // src/tools/create-validation-task.ts
695
+ import { z as z20 } from "zod";
696
+ function registerCreateValidationTask(server) {
697
+ server.registerTool(
698
+ "create_validation_task",
699
+ {
700
+ description: "Create a validation/test task in a project. After creating it, use add_system_to_task to declare which systems it exercises (so it goes stale when they change) and add_validation_dependency to order it after other tasks. Returns the new validationTaskId.",
701
+ inputSchema: {
702
+ projectId: z20.string().describe("projectId from list_projects"),
703
+ title: z20.string().min(1).describe("What this test validates"),
704
+ description: z20.string().optional().describe("Optional steps / details"),
705
+ kind: z20.enum(["auto", "manual"]).optional().describe(
706
+ "'auto' if runnable by a test runner, 'manual' if a human checks it. Defaults to manual."
707
+ ),
708
+ workItemId: z20.string().optional().describe(
709
+ "Optional workItemId to link this test to a feature for traceability"
710
+ )
711
+ },
712
+ annotations: {
713
+ readOnlyHint: false,
714
+ destructiveHint: false,
715
+ idempotentHint: false,
716
+ openWorldHint: false
717
+ }
718
+ },
719
+ async (args) => {
720
+ const client = getConvexClient();
721
+ const res = await client.action(api.mcp.createValidationTask, {
722
+ token: getToken(),
723
+ ...args
724
+ });
725
+ return {
726
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
727
+ };
728
+ }
729
+ );
730
+ }
731
+
732
+ // src/tools/add-system-to-task.ts
733
+ import { z as z21 } from "zod";
734
+ function registerAddSystemToTask(server) {
735
+ server.registerTool(
736
+ "add_system_to_task",
737
+ {
738
+ description: "Declare that a validation task COVERS (exercises) a system. Once linked, the task becomes 'stale' whenever that system is marked changed. Idempotent. Both IDs come from list_validations and must be in the same project.",
739
+ inputSchema: {
740
+ validationTaskId: z21.string().describe("validationTaskId from list_validations"),
741
+ systemId: z21.string().describe("systemId from list_validations")
742
+ },
743
+ annotations: {
744
+ readOnlyHint: false,
745
+ destructiveHint: false,
746
+ idempotentHint: true,
747
+ openWorldHint: false
748
+ }
749
+ },
750
+ async ({ validationTaskId, systemId }) => {
751
+ const client = getConvexClient();
752
+ const res = await client.action(api.mcp.addSystemToTask, {
753
+ token: getToken(),
754
+ validationTaskId,
755
+ systemId
756
+ });
757
+ return {
758
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
759
+ };
760
+ }
761
+ );
762
+ }
763
+
764
+ // src/tools/add-validation-dependency.ts
765
+ import { z as z22 } from "zod";
766
+ function registerAddValidationDependency(server) {
767
+ server.registerTool(
768
+ "add_validation_dependency",
769
+ {
770
+ description: "Make one validation task depend on another (validationTaskId depends on dependsOnTaskId). The dependent goes 'stale' whenever the upstream task is not passing or gets revalidated. Cycles and self-dependencies are rejected. Idempotent. Both IDs come from list_validations and must be in the same project.",
771
+ inputSchema: {
772
+ validationTaskId: z22.string().describe("The dependent (downstream) task"),
773
+ dependsOnTaskId: z22.string().describe("The dependency (upstream) task it relies on")
774
+ },
775
+ annotations: {
776
+ readOnlyHint: false,
777
+ destructiveHint: false,
778
+ idempotentHint: true,
779
+ openWorldHint: false
780
+ }
781
+ },
782
+ async ({ validationTaskId, dependsOnTaskId }) => {
783
+ const client = getConvexClient();
784
+ const res = await client.action(api.mcp.addValidationDependency, {
785
+ token: getToken(),
786
+ validationTaskId,
787
+ dependsOnTaskId
788
+ });
789
+ return {
790
+ content: [{ type: "text", text: JSON.stringify(res, null, 2) }]
791
+ };
792
+ }
793
+ );
794
+ }
795
+
796
+ // src/tools/list-labels.ts
797
+ import { z as z23 } from "zod";
798
+ function registerListLabels(server) {
799
+ server.registerTool(
800
+ "list_labels",
801
+ {
802
+ description: "List all labels defined in a project. Returns each label's id, name, and color. Use the labelId with add_label_to_work_item / remove_label_from_work_item.",
803
+ inputSchema: {
804
+ projectId: z23.string().describe("projectId from list_projects")
805
+ },
806
+ annotations: {
807
+ readOnlyHint: true,
808
+ openWorldHint: false
809
+ }
810
+ },
811
+ async (args) => {
812
+ const result = await getConvexClient().action(api.mcp.listLabels, {
813
+ token: getToken(),
814
+ ...args
815
+ });
816
+ return {
817
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
818
+ };
819
+ }
820
+ );
821
+ }
822
+
823
+ // src/tools/create-label.ts
824
+ import { z as z24 } from "zod";
825
+ function registerCreateLabel(server) {
826
+ server.registerTool(
827
+ "create_label",
828
+ {
829
+ description: "Create a label in a project (or return the existing one if the name is already taken \u2014 idempotent). Returns the labelId. Colors are optional; a neutral default is used when omitted.",
830
+ inputSchema: {
831
+ projectId: z24.string().describe("projectId from list_projects"),
832
+ name: z24.string().min(1).describe("Label name, unique within the project"),
833
+ color: z24.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color like #2563eb. Defaults to a neutral slate.")
834
+ },
835
+ annotations: {
836
+ readOnlyHint: false,
837
+ destructiveHint: false,
838
+ idempotentHint: true,
839
+ openWorldHint: false
840
+ }
841
+ },
842
+ async (args) => {
843
+ const result = await getConvexClient().action(api.mcp.createLabel, {
844
+ token: getToken(),
845
+ ...args
846
+ });
847
+ return {
848
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
849
+ };
850
+ }
851
+ );
852
+ }
853
+
854
+ // src/tools/add-label-to-work-item.ts
855
+ import { z as z25 } from "zod";
856
+ function registerAddLabelToWorkItem(server) {
857
+ server.registerTool(
858
+ "add_label_to_work_item",
859
+ {
860
+ description: "Attach a label to a work item (idempotent). The label and work item must be in the same project. Get labelIds from list_labels / create_label.",
861
+ inputSchema: {
862
+ workItemId: z25.string().describe("workItemId from list_work_items"),
863
+ labelId: z25.string().describe("labelId from list_labels or create_label")
864
+ },
865
+ annotations: {
866
+ readOnlyHint: false,
867
+ destructiveHint: false,
868
+ idempotentHint: true,
869
+ openWorldHint: false
870
+ }
871
+ },
872
+ async (args) => {
873
+ const result = await getConvexClient().action(api.mcp.setWorkItemLabel, {
874
+ token: getToken(),
875
+ ...args,
876
+ attached: true
877
+ });
878
+ return {
879
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
880
+ };
881
+ }
882
+ );
883
+ }
884
+
885
+ // src/tools/remove-label-from-work-item.ts
886
+ import { z as z26 } from "zod";
887
+ function registerRemoveLabelFromWorkItem(server) {
888
+ server.registerTool(
889
+ "remove_label_from_work_item",
890
+ {
891
+ description: "Remove a label from a work item (idempotent \u2014 a no-op if it wasn't attached).",
892
+ inputSchema: {
893
+ workItemId: z26.string().describe("workItemId from list_work_items"),
894
+ labelId: z26.string().describe("labelId from list_labels")
895
+ },
896
+ annotations: {
897
+ readOnlyHint: false,
898
+ destructiveHint: true,
899
+ idempotentHint: true,
900
+ openWorldHint: false
901
+ }
902
+ },
903
+ async (args) => {
904
+ const result = await getConvexClient().action(api.mcp.setWorkItemLabel, {
905
+ token: getToken(),
906
+ ...args,
907
+ attached: false
908
+ });
909
+ return {
910
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
911
+ };
912
+ }
913
+ );
914
+ }
915
+
916
+ // src/tools/list-milestones.ts
917
+ import { z as z27 } from "zod";
918
+ function registerListMilestones(server) {
919
+ server.registerTool(
920
+ "list_milestones",
921
+ {
922
+ description: "List all milestones in a project. Returns each milestone's id, name, description, dueDate (unix ms), and status. Use the milestoneId with add_item_to_milestone / remove_item_from_milestone.",
923
+ inputSchema: {
924
+ projectId: z27.string().describe("projectId from list_projects")
925
+ },
926
+ annotations: {
927
+ readOnlyHint: true,
928
+ openWorldHint: false
929
+ }
930
+ },
931
+ async (args) => {
932
+ const result = await getConvexClient().action(api.mcp.listMilestones, {
933
+ token: getToken(),
934
+ ...args
935
+ });
936
+ return {
937
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
938
+ };
939
+ }
940
+ );
941
+ }
942
+
943
+ // src/tools/create-milestone.ts
944
+ import { z as z28 } from "zod";
945
+ function registerCreateMilestone(server) {
946
+ server.registerTool(
947
+ "create_milestone",
948
+ {
949
+ description: "Create a milestone in a project. Returns the milestoneId. Good for grouping work into phases (e.g. 'Phase 1 \u2014 Web', 'Phase 2 \u2014 Native').",
950
+ inputSchema: {
951
+ projectId: z28.string().describe("projectId from list_projects"),
952
+ name: z28.string().min(1).describe("Milestone name"),
953
+ description: z28.string().optional().describe("Optional markdown description"),
954
+ dueDate: z28.number().optional().describe("Optional due date as a unix timestamp in milliseconds")
955
+ },
956
+ annotations: {
957
+ readOnlyHint: false,
958
+ destructiveHint: false,
959
+ idempotentHint: false,
960
+ openWorldHint: false
961
+ }
962
+ },
963
+ async (args) => {
964
+ const result = await getConvexClient().action(api.mcp.createMilestone, {
965
+ token: getToken(),
966
+ ...args
967
+ });
968
+ return {
969
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
970
+ };
971
+ }
972
+ );
973
+ }
974
+
975
+ // src/tools/add-item-to-milestone.ts
976
+ import { z as z29 } from "zod";
977
+ function registerAddItemToMilestone(server) {
978
+ server.registerTool(
979
+ "add_item_to_milestone",
980
+ {
981
+ description: "Associate a work item with a milestone (idempotent). Both must be in the same project.",
982
+ inputSchema: {
983
+ milestoneId: z29.string().describe("milestoneId from list_milestones or create_milestone"),
984
+ workItemId: z29.string().describe("workItemId from list_work_items")
985
+ },
986
+ annotations: {
987
+ readOnlyHint: false,
988
+ destructiveHint: false,
989
+ idempotentHint: true,
990
+ openWorldHint: false
991
+ }
992
+ },
993
+ async (args) => {
994
+ const result = await getConvexClient().action(
995
+ api.mcp.setWorkItemMilestone,
996
+ {
997
+ token: getToken(),
998
+ ...args,
999
+ attached: true
1000
+ }
1001
+ );
1002
+ return {
1003
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1004
+ };
1005
+ }
1006
+ );
1007
+ }
1008
+
1009
+ // src/tools/remove-item-from-milestone.ts
1010
+ import { z as z30 } from "zod";
1011
+ function registerRemoveItemFromMilestone(server) {
1012
+ server.registerTool(
1013
+ "remove_item_from_milestone",
1014
+ {
1015
+ description: "Remove a work item's association with a milestone (idempotent \u2014 a no-op if it wasn't associated).",
1016
+ inputSchema: {
1017
+ milestoneId: z30.string().describe("milestoneId from list_milestones"),
1018
+ workItemId: z30.string().describe("workItemId from list_work_items")
1019
+ },
1020
+ annotations: {
1021
+ readOnlyHint: false,
1022
+ destructiveHint: true,
1023
+ idempotentHint: true,
1024
+ openWorldHint: false
1025
+ }
1026
+ },
1027
+ async (args) => {
1028
+ const result = await getConvexClient().action(
1029
+ api.mcp.setWorkItemMilestone,
1030
+ {
1031
+ token: getToken(),
1032
+ ...args,
1033
+ attached: false
1034
+ }
1035
+ );
1036
+ return {
1037
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1038
+ };
1039
+ }
1040
+ );
1041
+ }
1042
+
1043
+ // src/server.ts
1044
+ function createServer() {
1045
+ const server = new McpServer({
1046
+ name: "nimara",
1047
+ version: "0.1.0"
1048
+ });
1049
+ registerListOrgs(server);
1050
+ registerListProjects(server);
1051
+ registerListWorkItems(server);
1052
+ registerListAiReviewQueue(server);
1053
+ registerCreateProject(server);
1054
+ registerCreateWorkItem(server);
1055
+ registerUpdateWorkItem(server);
1056
+ registerCreateDocument(server);
1057
+ registerUpdateDocument(server);
1058
+ registerListDocuments(server);
1059
+ registerCreateProjectLink(server);
1060
+ registerListProjectLinks(server);
1061
+ registerAddWorkItemImage(server);
1062
+ registerListWorkItemImages(server);
1063
+ registerAddWorkItemComment(server);
1064
+ registerListWorkItemComments(server);
1065
+ registerListValidations(server);
1066
+ registerRecordValidation(server);
1067
+ registerMarkSystemChanged(server);
1068
+ registerCreateSystem(server);
1069
+ registerCreateValidationTask(server);
1070
+ registerAddSystemToTask(server);
1071
+ registerAddValidationDependency(server);
1072
+ registerListLabels(server);
1073
+ registerCreateLabel(server);
1074
+ registerAddLabelToWorkItem(server);
1075
+ registerRemoveLabelFromWorkItem(server);
1076
+ registerListMilestones(server);
1077
+ registerCreateMilestone(server);
1078
+ registerAddItemToMilestone(server);
1079
+ registerRemoveItemFromMilestone(server);
1080
+ return server;
1081
+ }
1082
+
1083
+ // src/index.ts
1084
+ for (const m of ["log", "info", "warn", "debug"]) {
1085
+ console[m] = (...args) => console.error(...args);
1086
+ }
1087
+ async function main() {
1088
+ const server = createServer();
1089
+ const transport = new StdioServerTransport();
1090
+ await server.connect(transport);
1091
+ }
1092
+ main().catch((err) => {
1093
+ console.error("nimara-mcp: fatal error", err);
1094
+ process.exit(1);
1095
+ });
1096
+ //# sourceMappingURL=index.js.map