@nimara-app/mcp 0.2.0 → 0.4.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
@@ -6,6 +6,76 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  // src/server.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
 
9
+ // package.json
10
+ var package_default = {
11
+ name: "@nimara-app/mcp",
12
+ version: "0.4.0",
13
+ private: false,
14
+ mcpName: "io.github.langskip-studios/nimara",
15
+ description: "Nimara MCP server \u2014 AI tool integration for Claude, Codex, and others",
16
+ license: "MIT",
17
+ type: "module",
18
+ main: "dist/index.js",
19
+ bin: {
20
+ "nimara-mcp": "dist/index.js"
21
+ },
22
+ files: [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ keywords: [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "nimara",
30
+ "claude",
31
+ "ai",
32
+ "convex"
33
+ ],
34
+ repository: {
35
+ type: "git",
36
+ url: "git+https://github.com/Langskip-Studios/Nimara.git",
37
+ directory: "packages/mcp"
38
+ },
39
+ homepage: "https://github.com/Langskip-Studios/Nimara/tree/master/packages/mcp#readme",
40
+ publishConfig: {
41
+ access: "public"
42
+ },
43
+ scripts: {
44
+ build: "node build.mjs",
45
+ dev: "node build.mjs",
46
+ start: "node dist/index.js",
47
+ typecheck: "tsc --noEmit",
48
+ prepublishOnly: "npm run typecheck && npm run build"
49
+ },
50
+ dependencies: {
51
+ "@modelcontextprotocol/sdk": "^1.0.0",
52
+ convex: "^1.18.0",
53
+ zod: "^3.23.0"
54
+ },
55
+ devDependencies: {
56
+ "@nimara/convex": "workspace:*",
57
+ "@types/node": "^20.0.0",
58
+ esbuild: "^0.23.0",
59
+ typescript: "^5.0.0"
60
+ },
61
+ engines: {
62
+ node: ">=20"
63
+ }
64
+ };
65
+
66
+ // src/instructions.ts
67
+ var INSTRUCTIONS = `Nimara is the team's work tracker. Work items have a type (epic > feature > task > subtask), a status from the project's own workflow, and a displayId like NIM-T42 that people read and type.
68
+
69
+ Start with list_orgs \u2192 list_projects \u2192 get_active_block (the committed work; position 0 is next) and list_ai_review_queue (what a person has asked an agent to do; read \`answer\` first). Use get_work_items / search_work_items for items you already know of; list_work_items only to survey.
70
+
71
+ Titles are plain language a teammate understands at a glance: what is wrong or what changes for a person, not the file, function or mechanism. No em dashes, no "Area: thing, thing" shapes. Descriptions carry the why, acceptance criteria and file paths; a spec, design or long report goes in a document (create_document) linked from the item.
72
+
73
+ A status must be one of the project's \`statuses\` from list_projects, spelled exactly; never invent one. Labels come from list_labels; do not invent those either.
74
+
75
+ When you pick up an item: update_work_item { status: <in-progress status>, agentWorking: true }. That lights its row for 15 minutes; 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
+
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.`;
78
+
9
79
  // ../convex/convex/_generated/api.js
10
80
  import { anyApi, componentsGeneric } from "convex/server";
11
81
  var api = anyApi;
@@ -230,7 +300,7 @@ function registerListAiReviewQueue(server) {
230
300
  server.registerTool(
231
301
  "list_ai_review_queue",
232
302
  {
233
- 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.",
303
+ 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. Each item also carries recentComments (the last few, oldest first) and, when a person has written one, `answer`: the newest human comment. READ `answer` FIRST. When you flagged an item with a question and it is back in this queue, `answer` is the person's reply to that question; act on it rather than asking again. 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.",
234
304
  inputSchema: {
235
305
  projectId: z5.string().describe("projectId from list_projects"),
236
306
  limit: z5.number().int().min(1).max(200).optional().describe("Items per page (default 50, max 200)."),
@@ -257,8 +327,35 @@ function registerListAiReviewQueue(server) {
257
327
  );
258
328
  }
259
329
 
260
- // src/tools/create-work-item.ts
330
+ // src/tools/get-active-block.ts
261
331
  import { z as z6 } from "zod";
332
+ function registerGetActiveBlock(server) {
333
+ server.registerTool(
334
+ "get_active_block",
335
+ {
336
+ description: "The project's ACTIVE BLOCK: the small, ordered set of work a person has committed to right now. Call this BEFORE list_work_items when deciding what to work on \u2014 position 0 is the next thing to do, and the wider backlog is not on the table until the block is empty. Each item carries a lane: 'person' means a human owes it (flagged for review, or parked in a Review status) and you should not touch it; 'agent' means a human asked an agent to act on it, so it is yours first, and it carries recentComments and `answer` (the newest human comment: read it before anything else); 'open' is uncommitted-to-anyone work in priority order; 'done' is finished. Returns null when the project has no active block, in which case fall back to list_work_items. Descriptions are previewed; use get_work_items for the full text of an item you pick up. Call list_projects first for the projectId.",
337
+ inputSchema: {
338
+ projectId: z6.string().describe("projectId from list_projects")
339
+ },
340
+ annotations: {
341
+ readOnlyHint: true,
342
+ openWorldHint: false
343
+ }
344
+ },
345
+ async ({ projectId }) => {
346
+ const block = await getConvexClient().action(api.mcp.getActiveBlock, {
347
+ token: getToken(),
348
+ projectId
349
+ });
350
+ return {
351
+ content: [{ type: "text", text: JSON.stringify(block, null, 2) }]
352
+ };
353
+ }
354
+ );
355
+ }
356
+
357
+ // src/tools/create-work-item.ts
358
+ import { z as z7 } from "zod";
262
359
 
263
360
  // src/tools/titleGuidance.ts
264
361
  var TITLE_GUIDANCE = 'A title a teammate can understand at a glance, without opening the item or knowing the codebase. Say what is wrong or what changes FOR A PERSON, in plain words \u2014 not the mechanism, the file, or the symbol. Good: "Sorted list shows an expand arrow that does nothing". Bad: "Fix hasChildren computation in list.tsx". Good: "Long titles wrap to three lines and are hard to read". Bad: "Adjust COLUMNS.title min-width constant". Prefer ordinary words over internal abbreviations, and only name a file, function or identifier when it genuinely is the clearest way to say it (a tool or endpoint the user calls by name, for instance). No ticket prefixes or status markers \u2014 the board renders those already. Root cause, file paths, and reproduction steps belong in the description, which has room for them. Keep it to one plain clause. Do NOT use an em dash, and do not use the "Area: thing, other thing" shape \u2014 both are ways of stapling a second title on and they are what makes these hard to read. Bad: "Gantt: tighten row density, match app typography, stop the chart falling short of its container". Good: "Gantt chart looks nothing like the rest of the app". If the title needs a comma-spliced list to be accurate, that is a sign the item is really several items, not a sign it needs a longer title.';
@@ -266,6 +363,14 @@ var TITLE_GUIDANCE = 'A title a teammate can understand at a glance, without ope
266
363
  // src/tools/statusGuidance.ts
267
364
  var STATUS_GUIDANCE = 'Must be one of the names in the project\'s `statuses`, returned by list_projects. Read it rather than guessing \u2014 a status is the board column, so a name the workflow does not have is rejected, and before that check existed such items rendered in the first column while claiming otherwise. Match the existing name exactly, including its wording: if the workflow says "Review", do not write "In Review". Do NOT invent a status to capture something about an item. Statuses are shared across the whole project and aggregated on the dashboard, so each new one is a permanent extra band there. Anything that describes an item rather than locating it in the flow belongs in a LABEL \u2014 see create_label and add_label_to_work_item. Defaults to the first status when omitted.';
268
365
 
366
+ // ../convex/convex/lib/descriptionLimits.ts
367
+ var DESCRIPTION_MAX_CHARS = 32e3;
368
+ var DESCRIPTION_TOO_LONG = `A description can be at most ${DESCRIPTION_MAX_CHARS.toLocaleString("en-US")} characters. Put specs, research and long reports in a document and link it to the item.`;
369
+
370
+ // src/tools/descriptionGuidance.ts
371
+ var DESCRIPTION_MAX = DESCRIPTION_MAX_CHARS;
372
+ var DESCRIPTION_GUIDANCE = `Markdown. The what and the why: the problem, acceptance criteria, reproduction steps, file paths, root cause. Keep it to what someone picking the item up needs. A spec, a design, research, or a long report goes in a document (create_document) linked from here, not inline. At most ${DESCRIPTION_MAX_CHARS.toLocaleString("en-US")} characters.`;
373
+
269
374
  // src/tools/create-work-item.ts
270
375
  function registerCreateWorkItem(server) {
271
376
  server.registerTool(
@@ -273,17 +378,17 @@ function registerCreateWorkItem(server) {
273
378
  {
274
379
  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).",
275
380
  inputSchema: {
276
- projectId: z6.string().describe("projectId from list_projects"),
277
- type: z6.enum(["epic", "feature", "task", "subtask"]).describe("Item type \u2014 drives hierarchy validation"),
278
- title: z6.string().min(1).describe(TITLE_GUIDANCE),
279
- description: z6.string().optional().describe("Optional markdown body"),
280
- parentId: z6.string().optional().describe(
381
+ projectId: z7.string().describe("projectId from list_projects"),
382
+ type: z7.enum(["epic", "feature", "task", "subtask"]).describe("Item type \u2014 drives hierarchy validation"),
383
+ title: z7.string().min(1).describe(TITLE_GUIDANCE),
384
+ description: z7.string().max(DESCRIPTION_MAX).optional().describe(DESCRIPTION_GUIDANCE),
385
+ parentId: z7.string().optional().describe(
281
386
  "workItemId of the parent. Required for subtask, optional otherwise."
282
387
  ),
283
- status: z6.string().optional().describe(
388
+ status: z7.string().optional().describe(
284
389
  STATUS_GUIDANCE
285
390
  ),
286
- priority: z6.enum(["p0", "p1", "p2", "p3"]).optional().describe("p0 highest, p3 lowest. Defaults to p2.")
391
+ priority: z7.enum(["p0", "p1", "p2", "p3"]).optional().describe("p0 highest, p3 lowest. Defaults to p2.")
287
392
  },
288
393
  annotations: {
289
394
  readOnlyHint: false,
@@ -306,23 +411,23 @@ function registerCreateWorkItem(server) {
306
411
  }
307
412
 
308
413
  // src/tools/create-project.ts
309
- import { z as z7 } from "zod";
414
+ import { z as z8 } from "zod";
310
415
  function registerCreateProject(server) {
311
416
  server.registerTool(
312
417
  "create_project",
313
418
  {
314
419
  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.",
315
420
  inputSchema: {
316
- orgId: z7.string().describe("orgId from list_orgs"),
317
- name: z7.string().min(1).describe("Project name"),
318
- prefix: z7.string().min(1).max(8).describe(
421
+ orgId: z8.string().describe("orgId from list_orgs"),
422
+ name: z8.string().min(1).describe("Project name"),
423
+ prefix: z8.string().min(1).max(8).describe(
319
424
  "Short uppercase prefix for work item IDs (e.g. 'NIM'). Unique per org."
320
425
  ),
321
- description: z7.string().optional().describe("Optional markdown description"),
322
- isPrivate: z7.boolean().optional().describe(
426
+ description: z8.string().optional().describe("Optional markdown description"),
427
+ isPrivate: z8.boolean().optional().describe(
323
428
  "If true, only explicitly added members see it. Defaults to false."
324
429
  ),
325
- workflowTemplate: z7.enum(["default", "simple", "kanban", "bug"]).optional().describe(
430
+ workflowTemplate: z8.enum(["default", "simple", "kanban", "bug"]).optional().describe(
326
431
  "Status workflow. Defaults to 'default' (Backlog/Todo/In Progress/Review/Done)."
327
432
  )
328
433
  },
@@ -347,7 +452,7 @@ function registerCreateProject(server) {
347
452
  }
348
453
 
349
454
  // src/tools/update-work-item.ts
350
- import { z as z8 } from "zod";
455
+ import { z as z9 } from "zod";
351
456
 
352
457
  // src/tools/researchFormat.ts
353
458
  var RESEARCH_FORMAT = `When appending automated research to a description, use EXACTLY this format, after the human-written text:
@@ -383,22 +488,25 @@ function registerUpdateWorkItem(server) {
383
488
  {
384
489
  description: "Update an existing work item: title, description, status, priority, parent, or review flags. Any update also stamps the item as AI-touched.",
385
490
  inputSchema: {
386
- workItemId: z8.string().describe("workItemId from list_work_items"),
387
- title: z8.string().min(1).optional().describe(TITLE_GUIDANCE),
388
- description: z8.string().optional().describe(
389
- "Full markdown body, replacing what is there. " + RESEARCH_FORMAT
491
+ workItemId: z9.string().describe("workItemId from list_work_items"),
492
+ title: z9.string().min(1).optional().describe(TITLE_GUIDANCE),
493
+ description: z9.string().max(DESCRIPTION_MAX).optional().describe(
494
+ "Full markdown body, replacing what is there. " + DESCRIPTION_GUIDANCE + " " + RESEARCH_FORMAT
390
495
  ),
391
- status: z8.string().optional().describe(STATUS_GUIDANCE),
392
- priority: z8.enum(["p0", "p1", "p2", "p3"]).optional(),
393
- parentId: z8.string().nullable().optional().describe("New parent workItemId, null to move to root."),
394
- needsReview: z8.boolean().optional().describe(
496
+ status: z9.string().optional().describe(STATUS_GUIDANCE),
497
+ priority: z9.enum(["p0", "p1", "p2", "p3"]).optional(),
498
+ parentId: z9.string().nullable().optional().describe("New parent workItemId, null to move to root."),
499
+ needsReview: z9.boolean().optional().describe(
395
500
  "Flag the item for manual human review \u2014 set true when your enrichment was thin or uncertain."
396
501
  ),
397
- reviewReason: z8.string().optional().describe(
502
+ reviewReason: z9.string().optional().describe(
398
503
  "Short reason the item needs manual review (shown to the human)."
399
504
  ),
400
- aiReviewRequested: z8.boolean().optional().describe(
505
+ aiReviewRequested: z9.boolean().optional().describe(
401
506
  "The human\u2192AI review flag. Set false to clear it after you have reviewed/enriched an item from list_ai_review_queue."
507
+ ),
508
+ agentWorking: z9.boolean().optional().describe(
509
+ "Set true when you start working on this item: the app lights its row so the person can see it is in flight. It is a 15-minute lease renewed by any later MCP write on the item (updates, comments), by reading it (get_work_items, list_work_item_comments), or by heartbeat_work_item \u2014 call that every few minutes on a long task with no other Nimara calls, or the row goes quiet while you are still at it. Set false when you stop without finishing; moving the item to the last status clears it for you."
402
510
  )
403
511
  },
404
512
  annotations: {
@@ -421,7 +529,7 @@ function registerUpdateWorkItem(server) {
421
529
  }
422
530
 
423
531
  // src/tools/move-work-item.ts
424
- import { z as z9 } from "zod";
532
+ import { z as z10 } from "zod";
425
533
  function registerMoveWorkItem(server) {
426
534
  server.registerTool(
427
535
  "move_work_item",
@@ -432,8 +540,8 @@ function registerMoveWorkItem(server) {
432
540
  // changes, and labels/milestones do not survive.
433
541
  description: "Move a work item to a different project, taking its whole subtree with it \u2014 children and grandchildren move too, and the hierarchy is preserved. Both projects must be in the same organization, and you need write access to both. Not reversible by calling this again: the item is renumbered in the destination, so NIM-T44 might become SKA-T18 and the old ID stops resolving. Labels and milestone assignments are dropped, because both belong to the project being left; a status the destination doesn't define falls back to its first status. The response reports every old \u2192 new ID plus everything that was dropped or remapped. Comments, images, attached documents, PR links and history all follow the item. Call list_projects first for the target projectId.",
434
542
  inputSchema: {
435
- workItemId: z9.string().describe("workItemId from list_work_items \u2014 the item to move."),
436
- targetProjectId: z9.string().describe(
543
+ workItemId: z10.string().describe("workItemId from list_work_items \u2014 the item to move."),
544
+ targetProjectId: z10.string().describe(
437
545
  "projectId from list_projects \u2014 the destination. Must be in the same org as the item's current project."
438
546
  )
439
547
  },
@@ -462,20 +570,20 @@ function registerMoveWorkItem(server) {
462
570
  }
463
571
 
464
572
  // src/tools/create-document.ts
465
- import { z as z10 } from "zod";
573
+ import { z as z11 } from "zod";
466
574
  function registerCreateDocument(server) {
467
575
  server.registerTool(
468
576
  "create_document",
469
577
  {
470
578
  description: "Create a markdown project document. Use type 'prd' for project docs or 'fd' for feature/work-item documents.",
471
579
  inputSchema: {
472
- projectId: z10.string().describe("projectId from list_projects"),
473
- title: z10.string().min(1),
474
- type: z10.enum(["prd", "fd"]).describe(
580
+ projectId: z11.string().describe("projectId from list_projects"),
581
+ title: z11.string().min(1),
582
+ type: z11.enum(["prd", "fd"]).describe(
475
583
  "Document type. 'fd' is stored as the app's feature document type."
476
584
  ),
477
- workItemId: z10.string().optional().describe("Required for fd documents; omitted for prd."),
478
- content: z10.string().optional().describe("Optional initial markdown content.")
585
+ workItemId: z11.string().optional().describe("Required for fd documents; omitted for prd."),
586
+ content: z11.string().optional().describe("Optional initial markdown content.")
479
587
  },
480
588
  annotations: {
481
589
  readOnlyHint: false,
@@ -497,18 +605,18 @@ function registerCreateDocument(server) {
497
605
  }
498
606
 
499
607
  // src/tools/update-document.ts
500
- import { z as z11 } from "zod";
608
+ import { z as z12 } from "zod";
501
609
  function registerUpdateDocument(server) {
502
610
  server.registerTool(
503
611
  "update_document",
504
612
  {
505
613
  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.",
506
614
  inputSchema: {
507
- documentId: z11.string().describe("documentId from list_documents"),
508
- content: z11.string().optional().describe(
615
+ documentId: z12.string().describe("documentId from list_documents"),
616
+ content: z12.string().optional().describe(
509
617
  "New markdown content. Replaces the document's existing content."
510
618
  ),
511
- title: z11.string().min(1).optional().describe("New title (optional).")
619
+ title: z12.string().min(1).optional().describe("New title (optional).")
512
620
  },
513
621
  annotations: {
514
622
  readOnlyHint: false,
@@ -530,15 +638,15 @@ function registerUpdateDocument(server) {
530
638
  }
531
639
 
532
640
  // src/tools/list-documents.ts
533
- import { z as z12 } from "zod";
641
+ import { z as z13 } from "zod";
534
642
  function registerListDocuments(server) {
535
643
  server.registerTool(
536
644
  "list_documents",
537
645
  {
538
646
  description: "List PRD/FD markdown documents for a project, optionally filtered to one work item.",
539
647
  inputSchema: {
540
- projectId: z12.string().describe("projectId from list_projects"),
541
- workItemId: z12.string().optional().describe("Optional workItemId to list linked FD documents.")
648
+ projectId: z13.string().describe("projectId from list_projects"),
649
+ workItemId: z13.string().optional().describe("Optional workItemId to list linked FD documents.")
542
650
  },
543
651
  annotations: {
544
652
  readOnlyHint: true,
@@ -558,8 +666,8 @@ function registerListDocuments(server) {
558
666
  }
559
667
 
560
668
  // src/tools/create-project-link.ts
561
- import { z as z13 } from "zod";
562
- var projectLinkCategory = z13.enum([
669
+ import { z as z14 } from "zod";
670
+ var projectLinkCategory = z14.enum([
563
671
  "api",
564
672
  "dashboard",
565
673
  "documentation",
@@ -573,10 +681,10 @@ function registerCreateProjectLink(server) {
573
681
  {
574
682
  description: "Save a link on a project for resources like APIs, dashboards, documentation, repositories, or services.",
575
683
  inputSchema: {
576
- projectId: z13.string().describe("projectId from list_projects"),
577
- title: z13.string().min(1).max(120),
578
- url: z13.string().url().describe("http(s) URL to save"),
579
- description: z13.string().max(500).optional(),
684
+ projectId: z14.string().describe("projectId from list_projects"),
685
+ title: z14.string().min(1).max(120),
686
+ url: z14.string().url().describe("http(s) URL to save"),
687
+ description: z14.string().max(500).optional(),
580
688
  category: projectLinkCategory.optional().describe("Defaults to other.")
581
689
  },
582
690
  annotations: {
@@ -599,14 +707,14 @@ function registerCreateProjectLink(server) {
599
707
  }
600
708
 
601
709
  // src/tools/list-project-links.ts
602
- import { z as z14 } from "zod";
710
+ import { z as z15 } from "zod";
603
711
  function registerListProjectLinks(server) {
604
712
  server.registerTool(
605
713
  "list_project_links",
606
714
  {
607
715
  description: "List saved links for a project, such as APIs, dashboards, docs, repositories, and services.",
608
716
  inputSchema: {
609
- projectId: z14.string().describe("projectId from list_projects")
717
+ projectId: z15.string().describe("projectId from list_projects")
610
718
  },
611
719
  annotations: {
612
720
  readOnlyHint: true,
@@ -625,18 +733,105 @@ function registerListProjectLinks(server) {
625
733
  );
626
734
  }
627
735
 
736
+ // src/tools/list-scratch-notes.ts
737
+ import { z as z16 } from "zod";
738
+ function registerListScratchNotes(server) {
739
+ server.registerTool(
740
+ "list_scratch_notes",
741
+ {
742
+ 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.",
743
+ inputSchema: {
744
+ projectId: z16.string().describe("projectId from list_projects")
745
+ },
746
+ annotations: {
747
+ readOnlyHint: true,
748
+ openWorldHint: false
749
+ }
750
+ },
751
+ async ({ projectId }) => {
752
+ const notes = await getConvexClient().action(api.mcp.listScratchNotes, {
753
+ token: getToken(),
754
+ projectId
755
+ });
756
+ return {
757
+ content: [{ type: "text", text: JSON.stringify(notes, null, 2) }]
758
+ };
759
+ }
760
+ );
761
+ }
762
+
763
+ // src/tools/resolve-scratch-note.ts
764
+ import { z as z17 } from "zod";
765
+ function registerResolveScratchNote(server) {
766
+ server.registerTool(
767
+ "resolve_scratch_note",
768
+ {
769
+ 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.",
770
+ inputSchema: {
771
+ noteId: z17.string().describe("noteId from list_scratch_notes"),
772
+ workItemId: z17.string().optional().describe("The workItemId the note became, from create_work_item or list_work_items.")
773
+ },
774
+ annotations: {
775
+ readOnlyHint: false,
776
+ destructiveHint: false,
777
+ idempotentHint: true,
778
+ openWorldHint: false
779
+ }
780
+ },
781
+ async ({ noteId, workItemId }) => {
782
+ const result = await getConvexClient().action(api.mcp.resolveScratchNote, {
783
+ token: getToken(),
784
+ noteId,
785
+ workItemId
786
+ });
787
+ return {
788
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
789
+ };
790
+ }
791
+ );
792
+ }
793
+
794
+ // src/tools/heartbeat-work-item.ts
795
+ import { z as z18 } from "zod";
796
+ function registerHeartbeatWorkItem(server) {
797
+ server.registerTool(
798
+ "heartbeat_work_item",
799
+ {
800
+ 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.',
801
+ inputSchema: {
802
+ workItemId: z18.string().describe("workItemId of the item you are working on")
803
+ },
804
+ annotations: {
805
+ readOnlyHint: false,
806
+ destructiveHint: false,
807
+ idempotentHint: true,
808
+ openWorldHint: false
809
+ }
810
+ },
811
+ async ({ workItemId }) => {
812
+ const result = await getConvexClient().action(api.mcp.heartbeatWorkItem, {
813
+ token: getToken(),
814
+ workItemId
815
+ });
816
+ return {
817
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
818
+ };
819
+ }
820
+ );
821
+ }
822
+
628
823
  // src/tools/add-work-item-image.ts
629
- import { z as z15 } from "zod";
824
+ import { z as z19 } from "zod";
630
825
  function registerAddWorkItemImage(server) {
631
826
  server.registerTool(
632
827
  "add_work_item_image",
633
828
  {
634
829
  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.",
635
830
  inputSchema: {
636
- workItemId: z15.string().describe("workItemId from list_work_items"),
637
- imageUrl: z15.string().url().describe("Public or otherwise fetchable http(s) image URL"),
638
- filename: z15.string().optional().describe("Optional display filename"),
639
- altText: z15.string().optional().describe("Optional description/caption for the image")
831
+ workItemId: z19.string().describe("workItemId from list_work_items"),
832
+ imageUrl: z19.string().url().describe("Public or otherwise fetchable http(s) image URL"),
833
+ filename: z19.string().optional().describe("Optional display filename"),
834
+ altText: z19.string().optional().describe("Optional description/caption for the image")
640
835
  },
641
836
  annotations: {
642
837
  readOnlyHint: false,
@@ -658,14 +853,14 @@ function registerAddWorkItemImage(server) {
658
853
  }
659
854
 
660
855
  // src/tools/list-work-item-images.ts
661
- import { z as z16 } from "zod";
856
+ import { z as z20 } from "zod";
662
857
  function registerListWorkItemImages(server) {
663
858
  server.registerTool(
664
859
  "list_work_item_images",
665
860
  {
666
861
  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.",
667
862
  inputSchema: {
668
- workItemId: z16.string().describe("workItemId from list_work_items")
863
+ workItemId: z20.string().describe("workItemId from list_work_items")
669
864
  },
670
865
  annotations: {
671
866
  readOnlyHint: true,
@@ -688,16 +883,18 @@ function registerListWorkItemImages(server) {
688
883
  }
689
884
 
690
885
  // src/tools/add-work-item-comment.ts
691
- import { z as z17 } from "zod";
886
+ import { z as z21 } from "zod";
692
887
  function registerAddWorkItemComment(server) {
693
888
  server.registerTool(
694
889
  "add_work_item_comment",
695
890
  {
696
891
  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.",
697
892
  inputSchema: {
698
- workItemId: z17.string().describe("workItemId from list_work_items"),
699
- body: z17.string().min(1).describe("Comment text (markdown supported)"),
700
- source: z17.enum(["user", "ai"]).optional().describe('Who authored it; defaults to "ai" for MCP callers')
893
+ workItemId: z21.string().describe("workItemId from list_work_items"),
894
+ body: z21.string().min(1).describe(
895
+ "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."
896
+ ),
897
+ source: z21.enum(["user", "ai"]).optional().describe('Who authored it; defaults to "ai" for MCP callers')
701
898
  },
702
899
  annotations: {
703
900
  readOnlyHint: false,
@@ -722,14 +919,14 @@ function registerAddWorkItemComment(server) {
722
919
  }
723
920
 
724
921
  // src/tools/list-work-item-comments.ts
725
- import { z as z18 } from "zod";
922
+ import { z as z22 } from "zod";
726
923
  function registerListWorkItemComments(server) {
727
924
  server.registerTool(
728
925
  "list_work_item_comments",
729
926
  {
730
927
  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.",
731
928
  inputSchema: {
732
- workItemId: z18.string().describe("workItemId from list_work_items")
929
+ workItemId: z22.string().describe("workItemId from list_work_items")
733
930
  },
734
931
  annotations: {
735
932
  readOnlyHint: true,
@@ -752,14 +949,14 @@ function registerListWorkItemComments(server) {
752
949
  }
753
950
 
754
951
  // src/tools/list-validations.ts
755
- import { z as z19 } from "zod";
952
+ import { z as z23 } from "zod";
756
953
  function registerListValidations(server) {
757
954
  server.registerTool(
758
955
  "list_validations",
759
956
  {
760
957
  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.",
761
958
  inputSchema: {
762
- projectId: z19.string().describe("projectId from list_projects")
959
+ projectId: z23.string().describe("projectId from list_projects")
763
960
  },
764
961
  annotations: {
765
962
  readOnlyHint: true,
@@ -780,15 +977,15 @@ function registerListValidations(server) {
780
977
  }
781
978
 
782
979
  // src/tools/record-validation.ts
783
- import { z as z20 } from "zod";
980
+ import { z as z24 } from "zod";
784
981
  function registerRecordValidation(server) {
785
982
  server.registerTool(
786
983
  "record_validation",
787
984
  {
788
985
  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.",
789
986
  inputSchema: {
790
- validationTaskId: z20.string().describe("validationTaskId from list_validations"),
791
- result: z20.enum(["pass", "fail"]).describe("Outcome of running the test")
987
+ validationTaskId: z24.string().describe("validationTaskId from list_validations"),
988
+ result: z24.enum(["pass", "fail"]).describe("Outcome of running the test")
792
989
  },
793
990
  annotations: {
794
991
  readOnlyHint: false,
@@ -812,14 +1009,14 @@ function registerRecordValidation(server) {
812
1009
  }
813
1010
 
814
1011
  // src/tools/mark-system-changed.ts
815
- import { z as z21 } from "zod";
1012
+ import { z as z25 } from "zod";
816
1013
  function registerMarkSystemChanged(server) {
817
1014
  server.registerTool(
818
1015
  "mark_system_changed",
819
1016
  {
820
1017
  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.",
821
1018
  inputSchema: {
822
- systemId: z21.string().describe("systemId from list_validations")
1019
+ systemId: z25.string().describe("systemId from list_validations")
823
1020
  },
824
1021
  annotations: {
825
1022
  readOnlyHint: false,
@@ -842,16 +1039,16 @@ function registerMarkSystemChanged(server) {
842
1039
  }
843
1040
 
844
1041
  // src/tools/create-system.ts
845
- import { z as z22 } from "zod";
1042
+ import { z as z26 } from "zod";
846
1043
  function registerCreateSystem(server) {
847
1044
  server.registerTool(
848
1045
  "create_system",
849
1046
  {
850
1047
  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.",
851
1048
  inputSchema: {
852
- projectId: z22.string().describe("projectId from list_projects"),
853
- name: z22.string().min(1).describe("Short system name, e.g. 'Auth'"),
854
- description: z22.string().optional().describe("Optional details")
1049
+ projectId: z26.string().describe("projectId from list_projects"),
1050
+ name: z26.string().min(1).describe("Short system name, e.g. 'Auth'"),
1051
+ description: z26.string().optional().describe("Optional details")
855
1052
  },
856
1053
  annotations: {
857
1054
  readOnlyHint: false,
@@ -874,20 +1071,20 @@ function registerCreateSystem(server) {
874
1071
  }
875
1072
 
876
1073
  // src/tools/create-validation-task.ts
877
- import { z as z23 } from "zod";
1074
+ import { z as z27 } from "zod";
878
1075
  function registerCreateValidationTask(server) {
879
1076
  server.registerTool(
880
1077
  "create_validation_task",
881
1078
  {
882
1079
  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.",
883
1080
  inputSchema: {
884
- projectId: z23.string().describe("projectId from list_projects"),
885
- title: z23.string().min(1).describe("What this test validates"),
886
- description: z23.string().optional().describe("Optional steps / details"),
887
- kind: z23.enum(["auto", "manual"]).optional().describe(
1081
+ projectId: z27.string().describe("projectId from list_projects"),
1082
+ title: z27.string().min(1).describe("What this test validates"),
1083
+ description: z27.string().optional().describe("Optional steps / details"),
1084
+ kind: z27.enum(["auto", "manual"]).optional().describe(
888
1085
  "'auto' if runnable by a test runner, 'manual' if a human checks it. Defaults to manual."
889
1086
  ),
890
- workItemId: z23.string().optional().describe(
1087
+ workItemId: z27.string().optional().describe(
891
1088
  "Optional workItemId to link this test to a feature for traceability"
892
1089
  )
893
1090
  },
@@ -912,15 +1109,15 @@ function registerCreateValidationTask(server) {
912
1109
  }
913
1110
 
914
1111
  // src/tools/add-system-to-task.ts
915
- import { z as z24 } from "zod";
1112
+ import { z as z28 } from "zod";
916
1113
  function registerAddSystemToTask(server) {
917
1114
  server.registerTool(
918
1115
  "add_system_to_task",
919
1116
  {
920
1117
  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.",
921
1118
  inputSchema: {
922
- validationTaskId: z24.string().describe("validationTaskId from list_validations"),
923
- systemId: z24.string().describe("systemId from list_validations")
1119
+ validationTaskId: z28.string().describe("validationTaskId from list_validations"),
1120
+ systemId: z28.string().describe("systemId from list_validations")
924
1121
  },
925
1122
  annotations: {
926
1123
  readOnlyHint: false,
@@ -944,15 +1141,15 @@ function registerAddSystemToTask(server) {
944
1141
  }
945
1142
 
946
1143
  // src/tools/add-validation-dependency.ts
947
- import { z as z25 } from "zod";
1144
+ import { z as z29 } from "zod";
948
1145
  function registerAddValidationDependency(server) {
949
1146
  server.registerTool(
950
1147
  "add_validation_dependency",
951
1148
  {
952
1149
  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.",
953
1150
  inputSchema: {
954
- validationTaskId: z25.string().describe("The dependent (downstream) task"),
955
- dependsOnTaskId: z25.string().describe("The dependency (upstream) task it relies on")
1151
+ validationTaskId: z29.string().describe("The dependent (downstream) task"),
1152
+ dependsOnTaskId: z29.string().describe("The dependency (upstream) task it relies on")
956
1153
  },
957
1154
  annotations: {
958
1155
  readOnlyHint: false,
@@ -976,14 +1173,14 @@ function registerAddValidationDependency(server) {
976
1173
  }
977
1174
 
978
1175
  // src/tools/list-labels.ts
979
- import { z as z26 } from "zod";
1176
+ import { z as z30 } from "zod";
980
1177
  function registerListLabels(server) {
981
1178
  server.registerTool(
982
1179
  "list_labels",
983
1180
  {
984
1181
  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.",
985
1182
  inputSchema: {
986
- projectId: z26.string().describe("projectId from list_projects")
1183
+ projectId: z30.string().describe("projectId from list_projects")
987
1184
  },
988
1185
  annotations: {
989
1186
  readOnlyHint: true,
@@ -1003,16 +1200,16 @@ function registerListLabels(server) {
1003
1200
  }
1004
1201
 
1005
1202
  // src/tools/create-label.ts
1006
- import { z as z27 } from "zod";
1203
+ import { z as z31 } from "zod";
1007
1204
  function registerCreateLabel(server) {
1008
1205
  server.registerTool(
1009
1206
  "create_label",
1010
1207
  {
1011
1208
  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.",
1012
1209
  inputSchema: {
1013
- projectId: z27.string().describe("projectId from list_projects"),
1014
- name: z27.string().min(1).describe("Label name, unique within the project"),
1015
- color: z27.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color like #2563eb. Defaults to a neutral slate.")
1210
+ projectId: z31.string().describe("projectId from list_projects"),
1211
+ name: z31.string().min(1).describe("Label name, unique within the project"),
1212
+ color: z31.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("Hex color like #2563eb. Defaults to a neutral slate.")
1016
1213
  },
1017
1214
  annotations: {
1018
1215
  readOnlyHint: false,
@@ -1034,15 +1231,15 @@ function registerCreateLabel(server) {
1034
1231
  }
1035
1232
 
1036
1233
  // src/tools/add-label-to-work-item.ts
1037
- import { z as z28 } from "zod";
1234
+ import { z as z32 } from "zod";
1038
1235
  function registerAddLabelToWorkItem(server) {
1039
1236
  server.registerTool(
1040
1237
  "add_label_to_work_item",
1041
1238
  {
1042
1239
  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.",
1043
1240
  inputSchema: {
1044
- workItemId: z28.string().describe("workItemId from list_work_items"),
1045
- labelId: z28.string().describe("labelId from list_labels or create_label")
1241
+ workItemId: z32.string().describe("workItemId from list_work_items"),
1242
+ labelId: z32.string().describe("labelId from list_labels or create_label")
1046
1243
  },
1047
1244
  annotations: {
1048
1245
  readOnlyHint: false,
@@ -1065,15 +1262,15 @@ function registerAddLabelToWorkItem(server) {
1065
1262
  }
1066
1263
 
1067
1264
  // src/tools/remove-label-from-work-item.ts
1068
- import { z as z29 } from "zod";
1265
+ import { z as z33 } from "zod";
1069
1266
  function registerRemoveLabelFromWorkItem(server) {
1070
1267
  server.registerTool(
1071
1268
  "remove_label_from_work_item",
1072
1269
  {
1073
1270
  description: "Remove a label from a work item (idempotent \u2014 a no-op if it wasn't attached).",
1074
1271
  inputSchema: {
1075
- workItemId: z29.string().describe("workItemId from list_work_items"),
1076
- labelId: z29.string().describe("labelId from list_labels")
1272
+ workItemId: z33.string().describe("workItemId from list_work_items"),
1273
+ labelId: z33.string().describe("labelId from list_labels")
1077
1274
  },
1078
1275
  annotations: {
1079
1276
  readOnlyHint: false,
@@ -1096,14 +1293,14 @@ function registerRemoveLabelFromWorkItem(server) {
1096
1293
  }
1097
1294
 
1098
1295
  // src/tools/list-milestones.ts
1099
- import { z as z30 } from "zod";
1296
+ import { z as z34 } from "zod";
1100
1297
  function registerListMilestones(server) {
1101
1298
  server.registerTool(
1102
1299
  "list_milestones",
1103
1300
  {
1104
1301
  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.",
1105
1302
  inputSchema: {
1106
- projectId: z30.string().describe("projectId from list_projects")
1303
+ projectId: z34.string().describe("projectId from list_projects")
1107
1304
  },
1108
1305
  annotations: {
1109
1306
  readOnlyHint: true,
@@ -1123,17 +1320,17 @@ function registerListMilestones(server) {
1123
1320
  }
1124
1321
 
1125
1322
  // src/tools/create-milestone.ts
1126
- import { z as z31 } from "zod";
1323
+ import { z as z35 } from "zod";
1127
1324
  function registerCreateMilestone(server) {
1128
1325
  server.registerTool(
1129
1326
  "create_milestone",
1130
1327
  {
1131
1328
  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').",
1132
1329
  inputSchema: {
1133
- projectId: z31.string().describe("projectId from list_projects"),
1134
- name: z31.string().min(1).describe("Milestone name"),
1135
- description: z31.string().optional().describe("Optional markdown description"),
1136
- dueDate: z31.number().optional().describe("Optional due date as a unix timestamp in milliseconds")
1330
+ projectId: z35.string().describe("projectId from list_projects"),
1331
+ name: z35.string().min(1).describe("Milestone name"),
1332
+ description: z35.string().optional().describe("Optional markdown description"),
1333
+ dueDate: z35.number().optional().describe("Optional due date as a unix timestamp in milliseconds")
1137
1334
  },
1138
1335
  annotations: {
1139
1336
  readOnlyHint: false,
@@ -1155,15 +1352,15 @@ function registerCreateMilestone(server) {
1155
1352
  }
1156
1353
 
1157
1354
  // src/tools/add-item-to-milestone.ts
1158
- import { z as z32 } from "zod";
1355
+ import { z as z36 } from "zod";
1159
1356
  function registerAddItemToMilestone(server) {
1160
1357
  server.registerTool(
1161
1358
  "add_item_to_milestone",
1162
1359
  {
1163
1360
  description: "Associate a work item with a milestone (idempotent). Both must be in the same project.",
1164
1361
  inputSchema: {
1165
- milestoneId: z32.string().describe("milestoneId from list_milestones or create_milestone"),
1166
- workItemId: z32.string().describe("workItemId from list_work_items")
1362
+ milestoneId: z36.string().describe("milestoneId from list_milestones or create_milestone"),
1363
+ workItemId: z36.string().describe("workItemId from list_work_items")
1167
1364
  },
1168
1365
  annotations: {
1169
1366
  readOnlyHint: false,
@@ -1189,15 +1386,15 @@ function registerAddItemToMilestone(server) {
1189
1386
  }
1190
1387
 
1191
1388
  // src/tools/remove-item-from-milestone.ts
1192
- import { z as z33 } from "zod";
1389
+ import { z as z37 } from "zod";
1193
1390
  function registerRemoveItemFromMilestone(server) {
1194
1391
  server.registerTool(
1195
1392
  "remove_item_from_milestone",
1196
1393
  {
1197
1394
  description: "Remove a work item's association with a milestone (idempotent \u2014 a no-op if it wasn't associated).",
1198
1395
  inputSchema: {
1199
- milestoneId: z33.string().describe("milestoneId from list_milestones"),
1200
- workItemId: z33.string().describe("workItemId from list_work_items")
1396
+ milestoneId: z37.string().describe("milestoneId from list_milestones"),
1397
+ workItemId: z37.string().describe("workItemId from list_work_items")
1201
1398
  },
1202
1399
  annotations: {
1203
1400
  readOnlyHint: false,
@@ -1224,16 +1421,21 @@ function registerRemoveItemFromMilestone(server) {
1224
1421
 
1225
1422
  // src/server.ts
1226
1423
  function createServer() {
1227
- const server = new McpServer({
1228
- name: "nimara",
1229
- version: "0.1.0"
1230
- });
1424
+ const server = new McpServer(
1425
+ {
1426
+ name: "nimara",
1427
+ version: package_default.version
1428
+ },
1429
+ // Sent at connect time, before any tool is read (NIM-T189).
1430
+ { instructions: INSTRUCTIONS }
1431
+ );
1231
1432
  registerListOrgs(server);
1232
1433
  registerListProjects(server);
1233
1434
  registerListWorkItems(server);
1234
1435
  registerGetWorkItems(server);
1235
1436
  registerSearchWorkItems(server);
1236
1437
  registerListAiReviewQueue(server);
1438
+ registerGetActiveBlock(server);
1237
1439
  registerCreateProject(server);
1238
1440
  registerCreateWorkItem(server);
1239
1441
  registerUpdateWorkItem(server);
@@ -1243,6 +1445,9 @@ function createServer() {
1243
1445
  registerListDocuments(server);
1244
1446
  registerCreateProjectLink(server);
1245
1447
  registerListProjectLinks(server);
1448
+ registerListScratchNotes(server);
1449
+ registerResolveScratchNote(server);
1450
+ registerHeartbeatWorkItem(server);
1246
1451
  registerAddWorkItemImage(server);
1247
1452
  registerListWorkItemImages(server);
1248
1453
  registerAddWorkItemComment(server);