@nimara-app/mcp 0.6.0 → 0.8.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 CHANGED
@@ -9,7 +9,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "@nimara-app/mcp",
12
- version: "0.6.0",
12
+ version: "0.8.0",
13
13
  private: false,
14
14
  mcpName: "io.github.langskip-studios/nimara",
15
15
  description: "Nimara MCP server \u2014 AI tool integration for Claude, Codex, and others",
@@ -20,7 +20,8 @@ var package_default = {
20
20
  "nimara-mcp": "dist/index.js"
21
21
  },
22
22
  files: [
23
- "dist",
23
+ "dist/index.js",
24
+ "dist/index.js.map",
24
25
  "README.md"
25
26
  ],
26
27
  keywords: [
@@ -45,7 +46,8 @@ var package_default = {
45
46
  dev: "node build.mjs",
46
47
  start: "node dist/index.js",
47
48
  typecheck: "tsc --noEmit",
48
- prepublishOnly: "npm run typecheck && npm run build"
49
+ prepublishOnly: "npm run typecheck && npm run build",
50
+ test: "vitest run"
49
51
  },
50
52
  dependencies: {
51
53
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -53,10 +55,15 @@ var package_default = {
53
55
  zod: "^3.23.0"
54
56
  },
55
57
  devDependencies: {
58
+ "@clerk/hono": "^0.1.79",
59
+ "@clerk/mcp-tools": "^0.6.0",
56
60
  "@nimara/convex": "workspace:*",
57
61
  "@types/node": "^20.0.0",
58
62
  esbuild: "^0.23.0",
59
- typescript: "^5.0.0"
63
+ hono: "^4.13.8",
64
+ typescript: "^5.0.0",
65
+ vitest: "^2.1.9",
66
+ wrangler: "4.132.0"
60
67
  },
61
68
  engines: {
62
69
  node: ">=20"
@@ -74,13 +81,33 @@ A status must be one of the project's \`statuses\` from list_projects, spelled e
74
81
 
75
82
  When you pick up an item: update_work_item { status: <in-progress status>, agentWorking: true, model: <the model you run as> }. That lights its row for 15 minutes and lets the app say whose agent, and which, is on it; any write or read of the item renews it, and on a long stretch with no Nimara calls, heartbeat_work_item every few minutes keeps it lit. Post a comment at real milestones. If you stop early, agentWorking: false. Moving to the last status clears it.
76
83
 
77
- Mention a person in a comment as [@Name](user:<userId>) and they are notified. Append agent research to a description under the exact heading "## Research (auto)", never editing the human text above it. When you open a PR, put the displayId in its title.`;
84
+ People are userIds from list_project_members: mention one in a comment as [@Name](user:<userId>), or hand them the item with update_work_item's assigneeId / reviewerId; each notifies them. Append agent research to a description under the exact heading "## Research (auto)", never editing the human text above it. When you open a PR, put the displayId in its title and it links itself; unlink_pr removes a PR that landed on the wrong item.`;
78
85
 
79
86
  // src/convex.ts
80
87
  import { ConvexHttpClient } from "convex/browser";
88
+ import { ConvexError } from "convex/values";
81
89
  var cachedClient = null;
90
+ function describeConvexError(err) {
91
+ if (err instanceof ConvexError) {
92
+ const data = err.data;
93
+ if (typeof data === "string" && data.trim()) return data;
94
+ if (typeof data === "object" && data !== null) {
95
+ const { code, message } = data;
96
+ if ((code === "user" || code === "forbidden") && typeof message === "string" && message.trim()) {
97
+ return message;
98
+ }
99
+ if (code === "unauthenticated") return "This token is not valid. Generate a new one in Nimara \u2192 Settings \u2192 Personal Access Tokens.";
100
+ }
101
+ }
102
+ const raw = err instanceof Error ? err.message : String(err);
103
+ const requestId = /\[Request ID: ([^\]]+)\]/.exec(raw)?.[1];
104
+ if (requestId || err instanceof ConvexError) {
105
+ return requestId ? `Nimara could not complete that call (request ${requestId}). Check the arguments and try again.` : "Nimara could not complete that call. Check the arguments and try again.";
106
+ }
107
+ return `Nimara could not reach its backend: ${raw || "unknown transport error"}. Nothing is wrong with the arguments; check the server's NIMARA_CONVEX_URL and try again.`;
108
+ }
82
109
  function getConvexClient() {
83
- if (cachedClient) return cachedClient;
110
+ if (cachedClient) return wrap(cachedClient);
84
111
  const url = process.env.NIMARA_CONVEX_URL;
85
112
  if (!url) {
86
113
  throw new Error(
@@ -88,9 +115,22 @@ function getConvexClient() {
88
115
  );
89
116
  }
90
117
  cachedClient = new ConvexHttpClient(url);
91
- return cachedClient;
118
+ return wrap(cachedClient);
92
119
  }
93
- function getToken() {
120
+ function wrap(client) {
121
+ return {
122
+ action: async (ref, args) => {
123
+ try {
124
+ return await client.action(ref, args);
125
+ } catch (err) {
126
+ throw new Error(describeConvexError(err));
127
+ }
128
+ }
129
+ };
130
+ }
131
+ function getToken(extra) {
132
+ const fromRequest = extra?.authInfo?.token;
133
+ if (fromRequest) return fromRequest;
94
134
  const token = process.env.NIMARA_TOKEN;
95
135
  if (!token) {
96
136
  throw new Error(
@@ -122,13 +162,14 @@ function registerListOrgs(server) {
122
162
  description: "List all organizations (and the personal workspace) the authenticated user belongs to. Use this first to discover org IDs needed by other tools.",
123
163
  inputSchema: {},
124
164
  annotations: {
165
+ title: "List organizations",
125
166
  readOnlyHint: true,
126
167
  openWorldHint: false
127
168
  }
128
169
  },
129
- async () => {
170
+ async (_args, extra) => {
130
171
  const client = getConvexClient();
131
- const orgs = await client.action(api.mcp.listOrgs, { token: getToken() });
172
+ const orgs = await client.action(api.mcp.listOrgs, { token: getToken(extra) });
132
173
  return {
133
174
  content: [{ type: "text", text: JSON.stringify(orgs, null, 2) }]
134
175
  };
@@ -145,14 +186,15 @@ function registerListProjects(server) {
145
186
  description: "List projects in a given organization. Call list_orgs first to find the orgId. Each project includes `statuses` \u2014 its workflow in board order. Read that before setting a status on any item: those names are the only accepted values, and anything else is rejected.",
146
187
  inputSchema: { orgId: z.string().describe("orgId from list_orgs") },
147
188
  annotations: {
189
+ title: "List projects",
148
190
  readOnlyHint: true,
149
191
  openWorldHint: false
150
192
  }
151
193
  },
152
- async ({ orgId }) => {
194
+ async ({ orgId }, extra) => {
153
195
  const client = getConvexClient();
154
196
  const projects = await client.action(api.mcp.listProjects, {
155
- token: getToken(),
197
+ token: getToken(extra),
156
198
  orgId
157
199
  });
158
200
  return {
@@ -195,6 +237,7 @@ function registerListWorkItems(server) {
195
237
  )
196
238
  },
197
239
  annotations: {
240
+ title: "List work items",
198
241
  readOnlyHint: true,
199
242
  openWorldHint: false
200
243
  }
@@ -208,9 +251,9 @@ function registerListWorkItems(server) {
208
251
  type,
209
252
  parentId,
210
253
  needsReview
211
- }) => {
254
+ }, extra) => {
212
255
  const page = await getConvexClient().action(api.mcp.listWorkItems, {
213
- token: getToken(),
256
+ token: getToken(extra),
214
257
  projectId,
215
258
  limit,
216
259
  cursor,
@@ -251,13 +294,14 @@ function registerGetWorkItems(server) {
251
294
  )
252
295
  },
253
296
  annotations: {
297
+ title: "Get work items by id",
254
298
  readOnlyHint: true,
255
299
  openWorldHint: false
256
300
  }
257
301
  },
258
- async ({ displayIds, workItemIds, projectId }) => {
302
+ async ({ displayIds, workItemIds, projectId }, extra) => {
259
303
  const result = await getConvexClient().action(api.mcp.getWorkItems, {
260
- token: getToken(),
304
+ token: getToken(extra),
261
305
  displayIds,
262
306
  workItemIds,
263
307
  projectId
@@ -287,11 +331,14 @@ function registerSearchWorkItems(server) {
287
331
  ),
288
332
  limit: z4.number().int().min(1).max(25).optional().describe("Maximum results (default and maximum 25).")
289
333
  },
290
- annotations: { readOnlyHint: true }
334
+ annotations: {
335
+ title: "Search work items",
336
+ readOnlyHint: true
337
+ }
291
338
  },
292
- async ({ projectId, q, limit }) => {
339
+ async ({ projectId, q, limit }, extra) => {
293
340
  const result = await getConvexClient().action(api.mcp.searchWorkItems, {
294
- token: getToken(),
341
+ token: getToken(extra),
295
342
  projectId,
296
343
  q,
297
344
  limit
@@ -318,13 +365,14 @@ function registerListAiReviewQueue(server) {
318
365
  )
319
366
  },
320
367
  annotations: {
368
+ title: "List the AI review queue",
321
369
  readOnlyHint: true,
322
370
  openWorldHint: false
323
371
  }
324
372
  },
325
- async ({ projectId, limit, cursor }) => {
373
+ async ({ projectId, limit, cursor }, extra) => {
326
374
  const page = await getConvexClient().action(api.mcp.listAiReviewQueue, {
327
- token: getToken(),
375
+ token: getToken(extra),
328
376
  projectId,
329
377
  limit,
330
378
  cursor
@@ -347,13 +395,14 @@ function registerGetActiveBlock(server) {
347
395
  projectId: z6.string().describe("projectId from list_projects")
348
396
  },
349
397
  annotations: {
398
+ title: "Get the active block",
350
399
  readOnlyHint: true,
351
400
  openWorldHint: false
352
401
  }
353
402
  },
354
- async ({ projectId }) => {
403
+ async ({ projectId }, extra) => {
355
404
  const block = await getConvexClient().action(api.mcp.getActiveBlock, {
356
- token: getToken(),
405
+ token: getToken(extra),
357
406
  projectId
358
407
  });
359
408
  return {
@@ -400,16 +449,17 @@ function registerCreateWorkItem(server) {
400
449
  priority: z7.enum(["p0", "p1", "p2", "p3"]).optional().describe("p0 highest, p3 lowest. Defaults to p2.")
401
450
  },
402
451
  annotations: {
452
+ title: "Create a work item",
403
453
  readOnlyHint: false,
404
454
  destructiveHint: false,
405
455
  idempotentHint: false,
406
456
  openWorldHint: false
407
457
  }
408
458
  },
409
- async (args) => {
459
+ async (args, extra) => {
410
460
  const client = getConvexClient();
411
461
  const result = await client.action(api.mcp.createWorkItem, {
412
- token: getToken(),
462
+ token: getToken(extra),
413
463
  ...args
414
464
  });
415
465
  return {
@@ -432,7 +482,9 @@ function registerCreateProject(server) {
432
482
  prefix: z8.string().min(1).max(8).describe(
433
483
  "Short uppercase prefix for work item IDs (e.g. 'NIM'). Unique per org."
434
484
  ),
435
- description: z8.string().optional().describe("Optional markdown description"),
485
+ description: z8.string().max(280).optional().describe(
486
+ "Optional, and usually best left out. At most one or two plain sentences (280 characters) saying what the project is for \u2014 it shows behind an info button on the project page, so headings, stack notes, decisions and lineage do not belong here. Put that kind of context in a document (create_document) or the project's README instead."
487
+ ),
436
488
  isPrivate: z8.boolean().optional().describe(
437
489
  "If true, only explicitly added members see it. Defaults to false."
438
490
  ),
@@ -441,16 +493,17 @@ function registerCreateProject(server) {
441
493
  )
442
494
  },
443
495
  annotations: {
496
+ title: "Create a project",
444
497
  readOnlyHint: false,
445
498
  destructiveHint: false,
446
499
  idempotentHint: false,
447
500
  openWorldHint: false
448
501
  }
449
502
  },
450
- async (args) => {
503
+ async (args, extra) => {
451
504
  const client = getConvexClient();
452
505
  const result = await client.action(api.mcp.createProject, {
453
- token: getToken(),
506
+ token: getToken(extra),
454
507
  ...args
455
508
  });
456
509
  return {
@@ -495,7 +548,7 @@ function registerUpdateWorkItem(server) {
495
548
  server.registerTool(
496
549
  "update_work_item",
497
550
  {
498
- description: "Update an existing work item: title, description, status, priority, parent, or review flags. Any update also stamps the item as AI-touched.",
551
+ description: "Update an existing work item: title, description, status, priority, parent, assignee, reviewer, or review flags. Any update also stamps the item as AI-touched.",
499
552
  inputSchema: {
500
553
  workItemId: z9.string().describe("workItemId from list_work_items"),
501
554
  title: z9.string().min(1).optional().describe(TITLE_GUIDANCE),
@@ -505,6 +558,12 @@ function registerUpdateWorkItem(server) {
505
558
  status: z9.string().optional().describe(STATUS_GUIDANCE),
506
559
  priority: z9.enum(["p0", "p1", "p2", "p3"]).optional(),
507
560
  parentId: z9.string().nullable().optional().describe("New parent workItemId, null to move to root."),
561
+ assigneeId: z9.string().nullable().optional().describe(
562
+ "userId of the person to assign, from list_project_members; null to unassign. The person is notified. Must be someone on the project."
563
+ ),
564
+ reviewerId: z9.string().nullable().optional().describe(
565
+ "userId of the person to ask for review, from list_project_members; null to clear. The person is notified. Pair it with the Review status when you hand work over. Moving an item back up the workflow clears the reviewer unless the same call names one."
566
+ ),
508
567
  needsReview: z9.boolean().optional().describe(
509
568
  "Flag the item for manual human review \u2014 set true when your enrichment was thin or uncertain."
510
569
  ),
@@ -520,15 +579,16 @@ function registerUpdateWorkItem(server) {
520
579
  model: z9.string().optional().describe("With agentWorking: the model you are running as, e.g. claude-opus-5, so the app can say which agent is on this. Your harness is reported automatically.")
521
580
  },
522
581
  annotations: {
582
+ title: "Update a work item",
523
583
  readOnlyHint: false,
524
584
  destructiveHint: true,
525
585
  idempotentHint: true,
526
586
  openWorldHint: false
527
587
  }
528
588
  },
529
- async ({ model, ...args }) => {
589
+ async ({ model, ...args }, extra) => {
530
590
  const result = await getConvexClient().action(api.mcp.updateWorkItem, {
531
- token: getToken(),
591
+ token: getToken(extra),
532
592
  ...args,
533
593
  agent: agentArg(model)
534
594
  });
@@ -557,6 +617,7 @@ function registerMoveWorkItem(server) {
557
617
  )
558
618
  },
559
619
  annotations: {
620
+ title: "Move a work item to another project",
560
621
  readOnlyHint: false,
561
622
  // Renumbering display IDs and deleting label/milestone links are
562
623
  // overwrites, not additions — this belongs with the tools a client
@@ -568,10 +629,10 @@ function registerMoveWorkItem(server) {
568
629
  openWorldHint: false
569
630
  }
570
631
  },
571
- async ({ workItemId, targetProjectId }) => {
632
+ async ({ workItemId, targetProjectId }, extra) => {
572
633
  const result = await getConvexClient().action(
573
634
  api.mcp.moveWorkItemToProject,
574
- { token: getToken(), workItemId, targetProjectId }
635
+ { token: getToken(extra), workItemId, targetProjectId }
575
636
  );
576
637
  return {
577
638
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
@@ -586,26 +647,27 @@ function registerCreateDocument(server) {
586
647
  server.registerTool(
587
648
  "create_document",
588
649
  {
589
- description: "Create a markdown project document. Use type 'prd' for project docs or 'fd' for feature/work-item documents.",
650
+ description: "Create a markdown document in a project: a spec, a design, a report, research. Pass workItemId to link it to the item it is about. Leave type alone unless the document is THE project brief.",
590
651
  inputSchema: {
591
652
  projectId: z11.string().describe("projectId from list_projects"),
592
653
  title: z11.string().min(1),
593
- type: z11.enum(["prd", "fd"]).describe(
594
- "Document type. 'fd' is stored as the app's feature document type."
654
+ type: z11.enum(["note", "prd", "fd"]).optional().describe(
655
+ "Defaults to 'note', which is right for almost everything. 'prd' is reserved for the project's own product requirements brief, one per project, never linked to a work item. 'fd' is an older spelling of 'note' and still accepted."
595
656
  ),
596
- workItemId: z11.string().optional().describe("Required for fd documents; omitted for prd."),
657
+ workItemId: z11.string().optional().describe("The work item this document is about, so it shows on that item. Not allowed with type 'prd'."),
597
658
  content: z11.string().optional().describe("Optional initial markdown content.")
598
659
  },
599
660
  annotations: {
661
+ title: "Create a document",
600
662
  readOnlyHint: false,
601
663
  destructiveHint: false,
602
664
  idempotentHint: false,
603
665
  openWorldHint: false
604
666
  }
605
667
  },
606
- async (args) => {
668
+ async (args, extra) => {
607
669
  const result = await getConvexClient().action(api.mcp.createDocument, {
608
- token: getToken(),
670
+ token: getToken(extra),
609
671
  ...args
610
672
  });
611
673
  return {
@@ -630,15 +692,16 @@ function registerUpdateDocument(server) {
630
692
  title: z12.string().min(1).optional().describe("New title (optional).")
631
693
  },
632
694
  annotations: {
695
+ title: "Update a document",
633
696
  readOnlyHint: false,
634
697
  destructiveHint: true,
635
698
  idempotentHint: true,
636
699
  openWorldHint: false
637
700
  }
638
701
  },
639
- async (args) => {
702
+ async (args, extra) => {
640
703
  const result = await getConvexClient().action(api.mcp.updateDocument, {
641
- token: getToken(),
704
+ token: getToken(extra),
642
705
  ...args
643
706
  });
644
707
  return {
@@ -660,13 +723,14 @@ function registerListDocuments(server) {
660
723
  workItemId: z13.string().optional().describe("Optional workItemId to list linked FD documents.")
661
724
  },
662
725
  annotations: {
726
+ title: "List documents",
663
727
  readOnlyHint: true,
664
728
  openWorldHint: false
665
729
  }
666
730
  },
667
- async (args) => {
731
+ async (args, extra) => {
668
732
  const result = await getConvexClient().action(api.mcp.listDocuments, {
669
- token: getToken(),
733
+ token: getToken(extra),
670
734
  ...args
671
735
  });
672
736
  return {
@@ -699,15 +763,16 @@ function registerCreateProjectLink(server) {
699
763
  category: projectLinkCategory.optional().describe("Defaults to other.")
700
764
  },
701
765
  annotations: {
766
+ title: "Add a project link",
702
767
  readOnlyHint: false,
703
768
  destructiveHint: false,
704
769
  idempotentHint: false,
705
770
  openWorldHint: false
706
771
  }
707
772
  },
708
- async (args) => {
773
+ async (args, extra) => {
709
774
  const result = await getConvexClient().action(api.mcp.createProjectLink, {
710
- token: getToken(),
775
+ token: getToken(extra),
711
776
  ...args
712
777
  });
713
778
  return {
@@ -728,13 +793,14 @@ function registerListProjectLinks(server) {
728
793
  projectId: z15.string().describe("projectId from list_projects")
729
794
  },
730
795
  annotations: {
796
+ title: "List project links",
731
797
  readOnlyHint: true,
732
798
  openWorldHint: false
733
799
  }
734
800
  },
735
- async ({ projectId }) => {
801
+ async ({ projectId }, extra) => {
736
802
  const result = await getConvexClient().action(api.mcp.listProjectLinks, {
737
- token: getToken(),
803
+ token: getToken(extra),
738
804
  projectId
739
805
  });
740
806
  return {
@@ -744,24 +810,85 @@ function registerListProjectLinks(server) {
744
810
  );
745
811
  }
746
812
 
747
- // src/tools/list-scratch-notes.ts
813
+ // src/tools/list-project-members.ts
748
814
  import { z as z16 } from "zod";
815
+ function registerListProjectMembers(server) {
816
+ server.registerTool(
817
+ "list_project_members",
818
+ {
819
+ description: "Who can open a project: userId, name, email and role for everyone with access, the same list the app's assignee picker shows. Use it to turn a name into the userId that update_work_item's assigneeId and reviewerId take, and to build a mention \u2014 [@Name](user:<userId>) in a comment notifies that person.",
820
+ inputSchema: {
821
+ projectId: z16.string().describe("projectId from list_projects")
822
+ },
823
+ annotations: {
824
+ title: "List project members",
825
+ readOnlyHint: true,
826
+ openWorldHint: false
827
+ }
828
+ },
829
+ async ({ projectId }, extra) => {
830
+ const result = await getConvexClient().action(api.mcp.listProjectMembers, {
831
+ token: getToken(extra),
832
+ projectId
833
+ });
834
+ return {
835
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
836
+ };
837
+ }
838
+ );
839
+ }
840
+
841
+ // src/tools/unlink-pr.ts
842
+ import { z as z17 } from "zod";
843
+ function registerUnlinkPr(server) {
844
+ server.registerTool(
845
+ "unlink_pr",
846
+ {
847
+ description: "Take a pull request off a work item. PRs link themselves when their title or body names a display id, so a sample id in a PR description attaches that PR to a real item; this undoes it. Returns what was removed and what is still linked, so a wrong number can be corrected without another read. Removing the link does not touch the PR itself.",
848
+ inputSchema: {
849
+ workItemId: z17.string().describe("workItemId of the item the PR is wrongly on"),
850
+ prNumber: z17.number().int().positive().describe("The PR number, e.g. 354 for #354")
851
+ },
852
+ annotations: {
853
+ title: "Unlink a pull request",
854
+ readOnlyHint: false,
855
+ destructiveHint: true,
856
+ idempotentHint: true,
857
+ openWorldHint: false
858
+ }
859
+ },
860
+ async ({ workItemId, prNumber }, extra) => {
861
+ const result = await getConvexClient().action(api.mcp.unlinkPR, {
862
+ token: getToken(extra),
863
+ workItemId,
864
+ prNumber
865
+ });
866
+ return {
867
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
868
+ };
869
+ }
870
+ );
871
+ }
872
+
873
+ // src/tools/list-scratch-notes.ts
874
+ import { z as z18 } from "zod";
749
875
  function registerListScratchNotes(server) {
750
876
  server.registerTool(
751
877
  "list_scratch_notes",
752
878
  {
753
879
  description: "The token owner's OPEN scratch notes: thoughts and ideas they captured in the app with the `s` shortcut before deciding what to do with them. Returns the notes that belong with this project (tagged with it, or tagged with nothing), oldest first, each with noteId, body, projectId and createdAt. These are raw: a note may be one line or a paragraph, may describe one task or three, and may not be a task at all. Read each, decide what it should become (usually one or more work items via create_work_item, sometimes a comment on an existing item, sometimes nothing), do that, then call resolve_scratch_note with the noteId and the workItemId it became so the person sees where their note went. Ask in a comment rather than guess when a note is too thin to act on. Call list_projects first for the projectId.",
754
880
  inputSchema: {
755
- projectId: z16.string().describe("projectId from list_projects")
881
+ projectId: z18.string().describe("projectId from list_projects")
756
882
  },
757
883
  annotations: {
884
+ title: "List scratch notes",
758
885
  readOnlyHint: true,
759
886
  openWorldHint: false
760
887
  }
761
888
  },
762
- async ({ projectId }) => {
889
+ async ({ projectId }, extra) => {
763
890
  const notes = await getConvexClient().action(api.mcp.listScratchNotes, {
764
- token: getToken(),
891
+ token: getToken(extra),
765
892
  projectId
766
893
  });
767
894
  return {
@@ -772,26 +899,27 @@ function registerListScratchNotes(server) {
772
899
  }
773
900
 
774
901
  // src/tools/resolve-scratch-note.ts
775
- import { z as z17 } from "zod";
902
+ import { z as z19 } from "zod";
776
903
  function registerResolveScratchNote(server) {
777
904
  server.registerTool(
778
905
  "resolve_scratch_note",
779
906
  {
780
907
  description: "Mark a scratch note as used, recording the work item it became. Call this after acting on a note from list_scratch_notes: pass the noteId and the workItemId you created or updated from it (omit workItemId only when the note needed no item). The note leaves the person's open list and shows where it went. Only the token owner's own notes.",
781
908
  inputSchema: {
782
- noteId: z17.string().describe("noteId from list_scratch_notes"),
783
- workItemId: z17.string().optional().describe("The workItemId the note became, from create_work_item or list_work_items.")
909
+ noteId: z19.string().describe("noteId from list_scratch_notes"),
910
+ workItemId: z19.string().optional().describe("The workItemId the note became, from create_work_item or list_work_items.")
784
911
  },
785
912
  annotations: {
913
+ title: "Resolve a scratch note",
786
914
  readOnlyHint: false,
787
915
  destructiveHint: false,
788
916
  idempotentHint: true,
789
917
  openWorldHint: false
790
918
  }
791
919
  },
792
- async ({ noteId, workItemId }) => {
920
+ async ({ noteId, workItemId }, extra) => {
793
921
  const result = await getConvexClient().action(api.mcp.resolveScratchNote, {
794
- token: getToken(),
922
+ token: getToken(extra),
795
923
  noteId,
796
924
  workItemId
797
925
  });
@@ -803,26 +931,27 @@ function registerResolveScratchNote(server) {
803
931
  }
804
932
 
805
933
  // src/tools/heartbeat-work-item.ts
806
- import { z as z18 } from "zod";
934
+ import { z as z20 } from "zod";
807
935
  function registerHeartbeatWorkItem(server) {
808
936
  server.registerTool(
809
937
  "heartbeat_work_item",
810
938
  {
811
939
  description: 'Say you are still working on an item. Renews (or starts) its 15-minute "agent working" lease and does nothing else: no comment, no activity event, no change to the item. The app lights the row while the lease is live, so on a long task call this every few minutes or every handful of tool calls \u2014 otherwise the row goes quiet while you are still at it. Reads of the item (get_work_items, list_work_item_comments) also renew a live lease, and any update or comment does; this is for the stretches where you do none of those. Cheap and idempotent.',
812
940
  inputSchema: {
813
- workItemId: z18.string().describe("workItemId of the item you are working on"),
814
- model: z18.string().optional().describe("The model you are running as, e.g. claude-opus-5 or gpt-5, so the app can say which agent is on this. Your harness is reported automatically.")
941
+ workItemId: z20.string().describe("workItemId of the item you are working on"),
942
+ model: z20.string().optional().describe("The model you are running as, e.g. claude-opus-5 or gpt-5, so the app can say which agent is on this. Your harness is reported automatically.")
815
943
  },
816
944
  annotations: {
945
+ title: "Keep an agent lease alive",
817
946
  readOnlyHint: false,
818
947
  destructiveHint: false,
819
948
  idempotentHint: true,
820
949
  openWorldHint: false
821
950
  }
822
951
  },
823
- async ({ workItemId, model }) => {
952
+ async ({ workItemId, model }, extra) => {
824
953
  const result = await getConvexClient().action(api.mcp.heartbeatWorkItem, {
825
- token: getToken(),
954
+ token: getToken(extra),
826
955
  workItemId,
827
956
  agent: agentArg(model)
828
957
  });
@@ -834,26 +963,27 @@ function registerHeartbeatWorkItem(server) {
834
963
  }
835
964
 
836
965
  // src/tools/watch-work-item.ts
837
- import { z as z19 } from "zod";
966
+ import { z as z21 } from "zod";
838
967
  function registerWatchWorkItem(server) {
839
968
  server.registerTool(
840
969
  "watch_work_item",
841
970
  {
842
971
  description: "Put the token owner on (or take them off) an item's watch list. A watcher is notified when the item changes status, gets a comment, gets a reviewer, is archived or moved \u2014 not on title or description edits. The item's assignee and reviewer already hear about it. Use this when you start work the person will want to follow, or when they ask to keep an eye on something.",
843
972
  inputSchema: {
844
- workItemId: z19.string().describe("workItemId of the item"),
845
- watching: z19.boolean().default(true).describe("true to watch, false to stop")
973
+ workItemId: z21.string().describe("workItemId of the item"),
974
+ watching: z21.boolean().default(true).describe("true to watch, false to stop")
846
975
  },
847
976
  annotations: {
977
+ title: "Watch a work item",
848
978
  readOnlyHint: false,
849
979
  destructiveHint: false,
850
980
  idempotentHint: true,
851
981
  openWorldHint: false
852
982
  }
853
983
  },
854
- async ({ workItemId, watching }) => {
984
+ async ({ workItemId, watching }, extra) => {
855
985
  const result = await getConvexClient().action(api.mcp.watchWorkItem, {
856
- token: getToken(),
986
+ token: getToken(extra),
857
987
  workItemId,
858
988
  watching
859
989
  });
@@ -865,28 +995,29 @@ function registerWatchWorkItem(server) {
865
995
  }
866
996
 
867
997
  // src/tools/add-work-item-image.ts
868
- import { z as z20 } from "zod";
998
+ import { z as z22 } from "zod";
869
999
  function registerAddWorkItemImage(server) {
870
1000
  server.registerTool(
871
1001
  "add_work_item_image",
872
1002
  {
873
1003
  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.",
874
1004
  inputSchema: {
875
- workItemId: z20.string().describe("workItemId from list_work_items"),
876
- imageUrl: z20.string().url().describe("Public or otherwise fetchable http(s) image URL"),
877
- filename: z20.string().optional().describe("Optional display filename"),
878
- altText: z20.string().optional().describe("Optional description/caption for the image")
1005
+ workItemId: z22.string().describe("workItemId from list_work_items"),
1006
+ imageUrl: z22.string().url().describe("Public or otherwise fetchable http(s) image URL"),
1007
+ filename: z22.string().optional().describe("Optional display filename"),
1008
+ altText: z22.string().optional().describe("Optional description/caption for the image")
879
1009
  },
880
1010
  annotations: {
1011
+ title: "Attach an image to a work item",
881
1012
  readOnlyHint: false,
882
1013
  destructiveHint: false,
883
1014
  idempotentHint: false,
884
1015
  openWorldHint: false
885
1016
  }
886
1017
  },
887
- async (args) => {
1018
+ async (args, extra) => {
888
1019
  const result = await getConvexClient().action(api.mcp.addWorkItemImage, {
889
- token: getToken(),
1020
+ token: getToken(extra),
890
1021
  ...args
891
1022
  });
892
1023
  return {
@@ -897,25 +1028,26 @@ function registerAddWorkItemImage(server) {
897
1028
  }
898
1029
 
899
1030
  // src/tools/list-work-item-images.ts
900
- import { z as z21 } from "zod";
1031
+ import { z as z23 } from "zod";
901
1032
  function registerListWorkItemImages(server) {
902
1033
  server.registerTool(
903
1034
  "list_work_item_images",
904
1035
  {
905
1036
  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.",
906
1037
  inputSchema: {
907
- workItemId: z21.string().describe("workItemId from list_work_items")
1038
+ workItemId: z23.string().describe("workItemId from list_work_items")
908
1039
  },
909
1040
  annotations: {
1041
+ title: "List work item images",
910
1042
  readOnlyHint: true,
911
1043
  openWorldHint: false
912
1044
  }
913
1045
  },
914
- async ({ workItemId }) => {
1046
+ async ({ workItemId }, extra) => {
915
1047
  const result = await getConvexClient().action(
916
1048
  api.mcp.listWorkItemImages,
917
1049
  {
918
- token: getToken(),
1050
+ token: getToken(extra),
919
1051
  workItemId
920
1052
  }
921
1053
  );
@@ -927,31 +1059,32 @@ function registerListWorkItemImages(server) {
927
1059
  }
928
1060
 
929
1061
  // src/tools/add-work-item-comment.ts
930
- import { z as z22 } from "zod";
1062
+ import { z as z24 } from "zod";
931
1063
  function registerAddWorkItemComment(server) {
932
1064
  server.registerTool(
933
1065
  "add_work_item_comment",
934
1066
  {
935
1067
  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.",
936
1068
  inputSchema: {
937
- workItemId: z22.string().describe("workItemId from list_work_items"),
938
- body: z22.string().min(1).describe(
1069
+ workItemId: z24.string().describe("workItemId from list_work_items"),
1070
+ body: z24.string().min(1).describe(
939
1071
  "Comment text (markdown supported). To pull a person into the thread, mention them as [@Name](user:<userId>) \u2014 they are notified and the app renders it as a chip. userIds come from an item's assigneeId/reviewerId; there is no member lookup tool yet, so only mention ids you have seen."
940
1072
  ),
941
- source: z22.enum(["user", "ai"]).optional().describe('Who authored it; defaults to "ai" for MCP callers')
1073
+ source: z24.enum(["user", "ai"]).optional().describe('Who authored it; defaults to "ai" for MCP callers')
942
1074
  },
943
1075
  annotations: {
1076
+ title: "Comment on a work item",
944
1077
  readOnlyHint: false,
945
1078
  destructiveHint: false,
946
1079
  idempotentHint: false,
947
1080
  openWorldHint: false
948
1081
  }
949
1082
  },
950
- async (args) => {
1083
+ async (args, extra) => {
951
1084
  const result = await getConvexClient().action(
952
1085
  api.mcp.addWorkItemComment,
953
1086
  {
954
- token: getToken(),
1087
+ token: getToken(extra),
955
1088
  ...args,
956
1089
  agent: agentArg()
957
1090
  }
@@ -964,25 +1097,26 @@ function registerAddWorkItemComment(server) {
964
1097
  }
965
1098
 
966
1099
  // src/tools/list-work-item-comments.ts
967
- import { z as z23 } from "zod";
1100
+ import { z as z25 } from "zod";
968
1101
  function registerListWorkItemComments(server) {
969
1102
  server.registerTool(
970
1103
  "list_work_item_comments",
971
1104
  {
972
1105
  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.",
973
1106
  inputSchema: {
974
- workItemId: z23.string().describe("workItemId from list_work_items")
1107
+ workItemId: z25.string().describe("workItemId from list_work_items")
975
1108
  },
976
1109
  annotations: {
1110
+ title: "List work item comments",
977
1111
  readOnlyHint: true,
978
1112
  openWorldHint: false
979
1113
  }
980
1114
  },
981
- async (args) => {
1115
+ async (args, extra) => {
982
1116
  const result = await getConvexClient().action(
983
1117
  api.mcp.listWorkItemComments,
984
1118
  {
985
- token: getToken(),
1119
+ token: getToken(extra),
986
1120
  ...args
987
1121
  }
988
1122
  );
@@ -994,24 +1128,25 @@ function registerListWorkItemComments(server) {
994
1128
  }
995
1129
 
996
1130
  // src/tools/list-validations.ts
997
- import { z as z24 } from "zod";
1131
+ import { z as z26 } from "zod";
998
1132
  function registerListValidations(server) {
999
1133
  server.registerTool(
1000
1134
  "list_validations",
1001
1135
  {
1002
1136
  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.",
1003
1137
  inputSchema: {
1004
- projectId: z24.string().describe("projectId from list_projects")
1138
+ projectId: z26.string().describe("projectId from list_projects")
1005
1139
  },
1006
1140
  annotations: {
1141
+ title: "List validations",
1007
1142
  readOnlyHint: true,
1008
1143
  openWorldHint: false
1009
1144
  }
1010
1145
  },
1011
- async ({ projectId }) => {
1146
+ async ({ projectId }, extra) => {
1012
1147
  const client = getConvexClient();
1013
1148
  const graph = await client.action(api.mcp.listValidations, {
1014
- token: getToken(),
1149
+ token: getToken(extra),
1015
1150
  projectId
1016
1151
  });
1017
1152
  return {
@@ -1022,27 +1157,28 @@ function registerListValidations(server) {
1022
1157
  }
1023
1158
 
1024
1159
  // src/tools/record-validation.ts
1025
- import { z as z25 } from "zod";
1160
+ import { z as z27 } from "zod";
1026
1161
  function registerRecordValidation(server) {
1027
1162
  server.registerTool(
1028
1163
  "record_validation",
1029
1164
  {
1030
1165
  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.",
1031
1166
  inputSchema: {
1032
- validationTaskId: z25.string().describe("validationTaskId from list_validations"),
1033
- result: z25.enum(["pass", "fail"]).describe("Outcome of running the test")
1167
+ validationTaskId: z27.string().describe("validationTaskId from list_validations"),
1168
+ result: z27.enum(["pass", "fail"]).describe("Outcome of running the test")
1034
1169
  },
1035
1170
  annotations: {
1171
+ title: "Record a validation result",
1036
1172
  readOnlyHint: false,
1037
1173
  destructiveHint: true,
1038
1174
  idempotentHint: true,
1039
1175
  openWorldHint: false
1040
1176
  }
1041
1177
  },
1042
- async ({ validationTaskId, result }) => {
1178
+ async ({ validationTaskId, result }, extra) => {
1043
1179
  const client = getConvexClient();
1044
1180
  const res = await client.action(api.mcp.recordValidation, {
1045
- token: getToken(),
1181
+ token: getToken(extra),
1046
1182
  validationTaskId,
1047
1183
  result
1048
1184
  });
@@ -1054,26 +1190,27 @@ function registerRecordValidation(server) {
1054
1190
  }
1055
1191
 
1056
1192
  // src/tools/mark-system-changed.ts
1057
- import { z as z26 } from "zod";
1193
+ import { z as z28 } from "zod";
1058
1194
  function registerMarkSystemChanged(server) {
1059
1195
  server.registerTool(
1060
1196
  "mark_system_changed",
1061
1197
  {
1062
1198
  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.",
1063
1199
  inputSchema: {
1064
- systemId: z26.string().describe("systemId from list_validations")
1200
+ systemId: z28.string().describe("systemId from list_validations")
1065
1201
  },
1066
1202
  annotations: {
1203
+ title: "Mark a system as changed",
1067
1204
  readOnlyHint: false,
1068
1205
  destructiveHint: false,
1069
1206
  idempotentHint: false,
1070
1207
  openWorldHint: false
1071
1208
  }
1072
1209
  },
1073
- async ({ systemId }) => {
1210
+ async ({ systemId }, extra) => {
1074
1211
  const client = getConvexClient();
1075
1212
  const res = await client.action(api.mcp.markSystemChanged, {
1076
- token: getToken(),
1213
+ token: getToken(extra),
1077
1214
  systemId
1078
1215
  });
1079
1216
  return {
@@ -1084,28 +1221,29 @@ function registerMarkSystemChanged(server) {
1084
1221
  }
1085
1222
 
1086
1223
  // src/tools/create-system.ts
1087
- import { z as z27 } from "zod";
1224
+ import { z as z29 } from "zod";
1088
1225
  function registerCreateSystem(server) {
1089
1226
  server.registerTool(
1090
1227
  "create_system",
1091
1228
  {
1092
1229
  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.",
1093
1230
  inputSchema: {
1094
- projectId: z27.string().describe("projectId from list_projects"),
1095
- name: z27.string().min(1).describe("Short system name, e.g. 'Auth'"),
1096
- description: z27.string().optional().describe("Optional details")
1231
+ projectId: z29.string().describe("projectId from list_projects"),
1232
+ name: z29.string().min(1).describe("Short system name, e.g. 'Auth'"),
1233
+ description: z29.string().optional().describe("Optional details")
1097
1234
  },
1098
1235
  annotations: {
1236
+ title: "Create a system",
1099
1237
  readOnlyHint: false,
1100
1238
  destructiveHint: false,
1101
1239
  idempotentHint: false,
1102
1240
  openWorldHint: false
1103
1241
  }
1104
1242
  },
1105
- async (args) => {
1243
+ async (args, extra) => {
1106
1244
  const client = getConvexClient();
1107
1245
  const res = await client.action(api.mcp.createSystem, {
1108
- token: getToken(),
1246
+ token: getToken(extra),
1109
1247
  ...args
1110
1248
  });
1111
1249
  return {
@@ -1116,34 +1254,35 @@ function registerCreateSystem(server) {
1116
1254
  }
1117
1255
 
1118
1256
  // src/tools/create-validation-task.ts
1119
- import { z as z28 } from "zod";
1257
+ import { z as z30 } from "zod";
1120
1258
  function registerCreateValidationTask(server) {
1121
1259
  server.registerTool(
1122
1260
  "create_validation_task",
1123
1261
  {
1124
1262
  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.",
1125
1263
  inputSchema: {
1126
- projectId: z28.string().describe("projectId from list_projects"),
1127
- title: z28.string().min(1).describe("What this test validates"),
1128
- description: z28.string().optional().describe("Optional steps / details"),
1129
- kind: z28.enum(["auto", "manual"]).optional().describe(
1264
+ projectId: z30.string().describe("projectId from list_projects"),
1265
+ title: z30.string().min(1).describe("What this test validates"),
1266
+ description: z30.string().optional().describe("Optional steps / details"),
1267
+ kind: z30.enum(["auto", "manual"]).optional().describe(
1130
1268
  "'auto' if runnable by a test runner, 'manual' if a human checks it. Defaults to manual."
1131
1269
  ),
1132
- workItemId: z28.string().optional().describe(
1270
+ workItemId: z30.string().optional().describe(
1133
1271
  "Optional workItemId to link this test to a feature for traceability"
1134
1272
  )
1135
1273
  },
1136
1274
  annotations: {
1275
+ title: "Create a validation task",
1137
1276
  readOnlyHint: false,
1138
1277
  destructiveHint: false,
1139
1278
  idempotentHint: false,
1140
1279
  openWorldHint: false
1141
1280
  }
1142
1281
  },
1143
- async (args) => {
1282
+ async (args, extra) => {
1144
1283
  const client = getConvexClient();
1145
1284
  const res = await client.action(api.mcp.createValidationTask, {
1146
- token: getToken(),
1285
+ token: getToken(extra),
1147
1286
  ...args
1148
1287
  });
1149
1288
  return {
@@ -1154,27 +1293,28 @@ function registerCreateValidationTask(server) {
1154
1293
  }
1155
1294
 
1156
1295
  // src/tools/add-system-to-task.ts
1157
- import { z as z29 } from "zod";
1296
+ import { z as z31 } from "zod";
1158
1297
  function registerAddSystemToTask(server) {
1159
1298
  server.registerTool(
1160
1299
  "add_system_to_task",
1161
1300
  {
1162
1301
  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.",
1163
1302
  inputSchema: {
1164
- validationTaskId: z29.string().describe("validationTaskId from list_validations"),
1165
- systemId: z29.string().describe("systemId from list_validations")
1303
+ validationTaskId: z31.string().describe("validationTaskId from list_validations"),
1304
+ systemId: z31.string().describe("systemId from list_validations")
1166
1305
  },
1167
1306
  annotations: {
1307
+ title: "Link a system to a task",
1168
1308
  readOnlyHint: false,
1169
1309
  destructiveHint: false,
1170
1310
  idempotentHint: true,
1171
1311
  openWorldHint: false
1172
1312
  }
1173
1313
  },
1174
- async ({ validationTaskId, systemId }) => {
1314
+ async ({ validationTaskId, systemId }, extra) => {
1175
1315
  const client = getConvexClient();
1176
1316
  const res = await client.action(api.mcp.addSystemToTask, {
1177
- token: getToken(),
1317
+ token: getToken(extra),
1178
1318
  validationTaskId,
1179
1319
  systemId
1180
1320
  });
@@ -1186,27 +1326,28 @@ function registerAddSystemToTask(server) {
1186
1326
  }
1187
1327
 
1188
1328
  // src/tools/add-validation-dependency.ts
1189
- import { z as z30 } from "zod";
1329
+ import { z as z32 } from "zod";
1190
1330
  function registerAddValidationDependency(server) {
1191
1331
  server.registerTool(
1192
1332
  "add_validation_dependency",
1193
1333
  {
1194
1334
  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.",
1195
1335
  inputSchema: {
1196
- validationTaskId: z30.string().describe("The dependent (downstream) task"),
1197
- dependsOnTaskId: z30.string().describe("The dependency (upstream) task it relies on")
1336
+ validationTaskId: z32.string().describe("The dependent (downstream) task"),
1337
+ dependsOnTaskId: z32.string().describe("The dependency (upstream) task it relies on")
1198
1338
  },
1199
1339
  annotations: {
1340
+ title: "Add a validation dependency",
1200
1341
  readOnlyHint: false,
1201
1342
  destructiveHint: false,
1202
1343
  idempotentHint: true,
1203
1344
  openWorldHint: false
1204
1345
  }
1205
1346
  },
1206
- async ({ validationTaskId, dependsOnTaskId }) => {
1347
+ async ({ validationTaskId, dependsOnTaskId }, extra) => {
1207
1348
  const client = getConvexClient();
1208
1349
  const res = await client.action(api.mcp.addValidationDependency, {
1209
- token: getToken(),
1350
+ token: getToken(extra),
1210
1351
  validationTaskId,
1211
1352
  dependsOnTaskId
1212
1353
  });
@@ -1218,23 +1359,24 @@ function registerAddValidationDependency(server) {
1218
1359
  }
1219
1360
 
1220
1361
  // src/tools/list-labels.ts
1221
- import { z as z31 } from "zod";
1362
+ import { z as z33 } from "zod";
1222
1363
  function registerListLabels(server) {
1223
1364
  server.registerTool(
1224
1365
  "list_labels",
1225
1366
  {
1226
1367
  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.",
1227
1368
  inputSchema: {
1228
- projectId: z31.string().describe("projectId from list_projects")
1369
+ projectId: z33.string().describe("projectId from list_projects")
1229
1370
  },
1230
1371
  annotations: {
1372
+ title: "List labels",
1231
1373
  readOnlyHint: true,
1232
1374
  openWorldHint: false
1233
1375
  }
1234
1376
  },
1235
- async (args) => {
1377
+ async (args, extra) => {
1236
1378
  const result = await getConvexClient().action(api.mcp.listLabels, {
1237
- token: getToken(),
1379
+ token: getToken(extra),
1238
1380
  ...args
1239
1381
  });
1240
1382
  return {
@@ -1245,27 +1387,28 @@ function registerListLabels(server) {
1245
1387
  }
1246
1388
 
1247
1389
  // src/tools/create-label.ts
1248
- import { z as z32 } from "zod";
1390
+ import { z as z34 } from "zod";
1249
1391
  function registerCreateLabel(server) {
1250
1392
  server.registerTool(
1251
1393
  "create_label",
1252
1394
  {
1253
1395
  description: "Apply-or-seed a label. Returns the existing label if the name is already taken (idempotent), and otherwise creates it ONLY if the name is one of the curated ones. Any other new name is REFUSED \u2014 the reply carries `refused: true`, the reason, and `available`, the project's existing labels. Agents apply labels; they do not invent them, because a near-duplicate of an existing label splits the vocabulary silently. Prefer list_labels first. Colors are ignored for curated names so the vocabulary looks the same in every project.",
1254
1396
  inputSchema: {
1255
- projectId: z32.string().describe("projectId from list_projects"),
1256
- name: z32.string().min(1).describe("Label name, unique within the project"),
1257
- color: z32.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color like #2563eb. Defaults to a neutral slate.")
1397
+ projectId: z34.string().describe("projectId from list_projects"),
1398
+ name: z34.string().min(1).describe("Label name, unique within the project"),
1399
+ color: z34.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color like #2563eb. Defaults to a neutral slate.")
1258
1400
  },
1259
1401
  annotations: {
1402
+ title: "Create a label",
1260
1403
  readOnlyHint: false,
1261
1404
  destructiveHint: false,
1262
1405
  idempotentHint: true,
1263
1406
  openWorldHint: false
1264
1407
  }
1265
1408
  },
1266
- async (args) => {
1409
+ async (args, extra) => {
1267
1410
  const result = await getConvexClient().action(api.mcp.createLabel, {
1268
- token: getToken(),
1411
+ token: getToken(extra),
1269
1412
  ...args
1270
1413
  });
1271
1414
  return {
@@ -1276,26 +1419,27 @@ function registerCreateLabel(server) {
1276
1419
  }
1277
1420
 
1278
1421
  // src/tools/add-label-to-work-item.ts
1279
- import { z as z33 } from "zod";
1422
+ import { z as z35 } from "zod";
1280
1423
  function registerAddLabelToWorkItem(server) {
1281
1424
  server.registerTool(
1282
1425
  "add_label_to_work_item",
1283
1426
  {
1284
1427
  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.",
1285
1428
  inputSchema: {
1286
- workItemId: z33.string().describe("workItemId from list_work_items"),
1287
- labelId: z33.string().describe("labelId from list_labels or create_label")
1429
+ workItemId: z35.string().describe("workItemId from list_work_items"),
1430
+ labelId: z35.string().describe("labelId from list_labels or create_label")
1288
1431
  },
1289
1432
  annotations: {
1433
+ title: "Add a label to a work item",
1290
1434
  readOnlyHint: false,
1291
1435
  destructiveHint: false,
1292
1436
  idempotentHint: true,
1293
1437
  openWorldHint: false
1294
1438
  }
1295
1439
  },
1296
- async (args) => {
1440
+ async (args, extra) => {
1297
1441
  const result = await getConvexClient().action(api.mcp.setWorkItemLabel, {
1298
- token: getToken(),
1442
+ token: getToken(extra),
1299
1443
  ...args,
1300
1444
  attached: true
1301
1445
  });
@@ -1307,26 +1451,27 @@ function registerAddLabelToWorkItem(server) {
1307
1451
  }
1308
1452
 
1309
1453
  // src/tools/remove-label-from-work-item.ts
1310
- import { z as z34 } from "zod";
1454
+ import { z as z36 } from "zod";
1311
1455
  function registerRemoveLabelFromWorkItem(server) {
1312
1456
  server.registerTool(
1313
1457
  "remove_label_from_work_item",
1314
1458
  {
1315
1459
  description: "Remove a label from a work item (idempotent \u2014 a no-op if it wasn't attached).",
1316
1460
  inputSchema: {
1317
- workItemId: z34.string().describe("workItemId from list_work_items"),
1318
- labelId: z34.string().describe("labelId from list_labels")
1461
+ workItemId: z36.string().describe("workItemId from list_work_items"),
1462
+ labelId: z36.string().describe("labelId from list_labels")
1319
1463
  },
1320
1464
  annotations: {
1465
+ title: "Remove a label from a work item",
1321
1466
  readOnlyHint: false,
1322
1467
  destructiveHint: true,
1323
1468
  idempotentHint: true,
1324
1469
  openWorldHint: false
1325
1470
  }
1326
1471
  },
1327
- async (args) => {
1472
+ async (args, extra) => {
1328
1473
  const result = await getConvexClient().action(api.mcp.setWorkItemLabel, {
1329
- token: getToken(),
1474
+ token: getToken(extra),
1330
1475
  ...args,
1331
1476
  attached: false
1332
1477
  });
@@ -1338,23 +1483,24 @@ function registerRemoveLabelFromWorkItem(server) {
1338
1483
  }
1339
1484
 
1340
1485
  // src/tools/list-milestones.ts
1341
- import { z as z35 } from "zod";
1486
+ import { z as z37 } from "zod";
1342
1487
  function registerListMilestones(server) {
1343
1488
  server.registerTool(
1344
1489
  "list_milestones",
1345
1490
  {
1346
1491
  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.",
1347
1492
  inputSchema: {
1348
- projectId: z35.string().describe("projectId from list_projects")
1493
+ projectId: z37.string().describe("projectId from list_projects")
1349
1494
  },
1350
1495
  annotations: {
1496
+ title: "List milestones",
1351
1497
  readOnlyHint: true,
1352
1498
  openWorldHint: false
1353
1499
  }
1354
1500
  },
1355
- async (args) => {
1501
+ async (args, extra) => {
1356
1502
  const result = await getConvexClient().action(api.mcp.listMilestones, {
1357
- token: getToken(),
1503
+ token: getToken(extra),
1358
1504
  ...args
1359
1505
  });
1360
1506
  return {
@@ -1365,28 +1511,29 @@ function registerListMilestones(server) {
1365
1511
  }
1366
1512
 
1367
1513
  // src/tools/create-milestone.ts
1368
- import { z as z36 } from "zod";
1514
+ import { z as z38 } from "zod";
1369
1515
  function registerCreateMilestone(server) {
1370
1516
  server.registerTool(
1371
1517
  "create_milestone",
1372
1518
  {
1373
1519
  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').",
1374
1520
  inputSchema: {
1375
- projectId: z36.string().describe("projectId from list_projects"),
1376
- name: z36.string().min(1).describe("Milestone name"),
1377
- description: z36.string().optional().describe("Optional markdown description"),
1378
- dueDate: z36.number().optional().describe("Optional due date as a unix timestamp in milliseconds")
1521
+ projectId: z38.string().describe("projectId from list_projects"),
1522
+ name: z38.string().min(1).describe("Milestone name"),
1523
+ description: z38.string().optional().describe("Optional markdown description"),
1524
+ dueDate: z38.number().optional().describe("Optional due date as a unix timestamp in milliseconds")
1379
1525
  },
1380
1526
  annotations: {
1527
+ title: "Create a milestone",
1381
1528
  readOnlyHint: false,
1382
1529
  destructiveHint: false,
1383
1530
  idempotentHint: false,
1384
1531
  openWorldHint: false
1385
1532
  }
1386
1533
  },
1387
- async (args) => {
1534
+ async (args, extra) => {
1388
1535
  const result = await getConvexClient().action(api.mcp.createMilestone, {
1389
- token: getToken(),
1536
+ token: getToken(extra),
1390
1537
  ...args
1391
1538
  });
1392
1539
  return {
@@ -1397,28 +1544,29 @@ function registerCreateMilestone(server) {
1397
1544
  }
1398
1545
 
1399
1546
  // src/tools/add-item-to-milestone.ts
1400
- import { z as z37 } from "zod";
1547
+ import { z as z39 } from "zod";
1401
1548
  function registerAddItemToMilestone(server) {
1402
1549
  server.registerTool(
1403
1550
  "add_item_to_milestone",
1404
1551
  {
1405
1552
  description: "Associate a work item with a milestone (idempotent). Both must be in the same project.",
1406
1553
  inputSchema: {
1407
- milestoneId: z37.string().describe("milestoneId from list_milestones or create_milestone"),
1408
- workItemId: z37.string().describe("workItemId from list_work_items")
1554
+ milestoneId: z39.string().describe("milestoneId from list_milestones or create_milestone"),
1555
+ workItemId: z39.string().describe("workItemId from list_work_items")
1409
1556
  },
1410
1557
  annotations: {
1558
+ title: "Add an item to a milestone",
1411
1559
  readOnlyHint: false,
1412
1560
  destructiveHint: false,
1413
1561
  idempotentHint: true,
1414
1562
  openWorldHint: false
1415
1563
  }
1416
1564
  },
1417
- async (args) => {
1565
+ async (args, extra) => {
1418
1566
  const result = await getConvexClient().action(
1419
1567
  api.mcp.setWorkItemMilestone,
1420
1568
  {
1421
- token: getToken(),
1569
+ token: getToken(extra),
1422
1570
  ...args,
1423
1571
  attached: true
1424
1572
  }
@@ -1431,28 +1579,29 @@ function registerAddItemToMilestone(server) {
1431
1579
  }
1432
1580
 
1433
1581
  // src/tools/remove-item-from-milestone.ts
1434
- import { z as z38 } from "zod";
1582
+ import { z as z40 } from "zod";
1435
1583
  function registerRemoveItemFromMilestone(server) {
1436
1584
  server.registerTool(
1437
1585
  "remove_item_from_milestone",
1438
1586
  {
1439
1587
  description: "Remove a work item's association with a milestone (idempotent \u2014 a no-op if it wasn't associated).",
1440
1588
  inputSchema: {
1441
- milestoneId: z38.string().describe("milestoneId from list_milestones"),
1442
- workItemId: z38.string().describe("workItemId from list_work_items")
1589
+ milestoneId: z40.string().describe("milestoneId from list_milestones"),
1590
+ workItemId: z40.string().describe("workItemId from list_work_items")
1443
1591
  },
1444
1592
  annotations: {
1593
+ title: "Remove an item from a milestone",
1445
1594
  readOnlyHint: false,
1446
1595
  destructiveHint: true,
1447
1596
  idempotentHint: true,
1448
1597
  openWorldHint: false
1449
1598
  }
1450
1599
  },
1451
- async (args) => {
1600
+ async (args, extra) => {
1452
1601
  const result = await getConvexClient().action(
1453
1602
  api.mcp.setWorkItemMilestone,
1454
1603
  {
1455
- token: getToken(),
1604
+ token: getToken(extra),
1456
1605
  ...args,
1457
1606
  attached: false
1458
1607
  }
@@ -1491,6 +1640,8 @@ function createServer() {
1491
1640
  registerListDocuments(server);
1492
1641
  registerCreateProjectLink(server);
1493
1642
  registerListProjectLinks(server);
1643
+ registerListProjectMembers(server);
1644
+ registerUnlinkPr(server);
1494
1645
  registerListScratchNotes(server);
1495
1646
  registerResolveScratchNote(server);
1496
1647
  registerHeartbeatWorkItem(server);