@naumu/mcp 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +176 -34
  2. package/dist/index.js +546 -128
  3. package/package.json +8 -7
  4. package/server.json +58 -0
package/dist/index.js CHANGED
@@ -29,18 +29,71 @@ function safeErrorMessage(status, upstream) {
29
29
  return `Request failed with status ${status}.`;
30
30
  }
31
31
 
32
+ // ../mcp-core/src/version.ts
33
+ var NAUMU_MCP_VERSION = "0.12.0";
34
+
32
35
  // ../mcp-core/src/client.ts
36
+ var HEADER_VALUE_MAX_LENGTH = 100;
37
+ var sanitizeHeaderValue = (value) => {
38
+ const cleaned = value.replace(/[^\x20-\x7e]/g, "").trim();
39
+ return cleaned.length > 0 ? cleaned.slice(0, HEADER_VALUE_MAX_LENGTH) : void 0;
40
+ };
33
41
  var NaumuClient = class {
34
42
  baseUrl;
35
43
  apiKey;
36
- constructor(baseUrl, apiKey2) {
44
+ reportMcpClientHeaders;
45
+ mcpClientInfo;
46
+ pendingToolName;
47
+ constructor(baseUrl, apiKey2, options = {}) {
37
48
  this.baseUrl = baseUrl.replace(/\/+$/, "");
38
49
  this.apiKey = apiKey2;
50
+ this.reportMcpClientHeaders = options.reportMcpClientHeaders === true;
51
+ }
52
+ /**
53
+ * Record the harness identity learned from the MCP initialize handshake
54
+ * (see `server.server.getClientVersion()` in the stdio entrypoint).
55
+ */
56
+ setMcpClientInfo(info) {
57
+ this.mcpClientInfo = info;
58
+ }
59
+ /**
60
+ * Mark which MCP tool is about to run. Set by the tool-attribution wrapper
61
+ * immediately before a handler executes.
62
+ */
63
+ setPendingToolName(toolName) {
64
+ this.pendingToolName = toolName;
65
+ }
66
+ /**
67
+ * Consume the pending tool name — FIRST request only.
68
+ *
69
+ * One MCP tool call can fan out into several REST calls (e.g. a schema fetch
70
+ * before the write). Consuming on the first request means the backend counts
71
+ * exactly one `mcp_tool_call` per tool invocation; the follow-up calls arrive
72
+ * without the tool header and are deliberately not tracked.
73
+ */
74
+ consumePendingToolName() {
75
+ const toolName = this.pendingToolName;
76
+ this.pendingToolName = void 0;
77
+ return toolName;
78
+ }
79
+ /** Opt-in usage-attribution headers. Empty unless `reportMcpClientHeaders` is on. */
80
+ mcpHeaders() {
81
+ if (!this.reportMcpClientHeaders) return {};
82
+ const headers = { "X-Naumu-Mcp-Version": NAUMU_MCP_VERSION };
83
+ const clientName = this.mcpClientInfo?.name ? sanitizeHeaderValue(this.mcpClientInfo.name) : void 0;
84
+ if (clientName) headers["X-Naumu-Mcp-Client"] = clientName;
85
+ const clientVersion = this.mcpClientInfo?.version ? sanitizeHeaderValue(this.mcpClientInfo.version) : void 0;
86
+ if (clientVersion) headers["X-Naumu-Mcp-Client-Version"] = clientVersion;
87
+ const pendingTool = this.consumePendingToolName();
88
+ const toolName = pendingTool ? sanitizeHeaderValue(pendingTool) : void 0;
89
+ if (toolName) headers["X-Naumu-Mcp-Tool"] = toolName;
90
+ return headers;
39
91
  }
40
92
  headers() {
41
93
  return {
42
94
  "Authorization": `Bearer ${this.apiKey}`,
43
- "Content-Type": "application/json"
95
+ "Content-Type": "application/json",
96
+ ...this.mcpHeaders()
44
97
  };
45
98
  }
46
99
  async handleResponse(res) {
@@ -70,7 +123,7 @@ var NaumuClient = class {
70
123
  async getBinary(path) {
71
124
  const res = await fetch(`${this.baseUrl}${path}`, {
72
125
  method: "GET",
73
- headers: { Authorization: `Bearer ${this.apiKey}` }
126
+ headers: { Authorization: `Bearer ${this.apiKey}`, ...this.mcpHeaders() }
74
127
  });
75
128
  if (!res.ok) {
76
129
  const upstream = await res.text().catch(() => "Unknown error");
@@ -89,6 +142,33 @@ var NaumuClient = class {
89
142
  contentType: res.headers.get("content-type") ?? "application/octet-stream"
90
143
  };
91
144
  }
145
+ /**
146
+ * Follow a redirect-issuing endpoint one hop and return the `Location` it
147
+ * points at, without fetching the target. Any non-3xx answer is a failure
148
+ * (403/404/…) and is mapped through the same safe-error path as the rest.
149
+ */
150
+ async getRedirectLocation(path) {
151
+ const res = await fetch(`${this.baseUrl}${path}`, {
152
+ method: "GET",
153
+ redirect: "manual",
154
+ headers: { Authorization: `Bearer ${this.apiKey}`, ...this.mcpHeaders() }
155
+ });
156
+ if (res.status >= 300 && res.status < 400) {
157
+ const location = res.headers.get("location");
158
+ if (location) return location;
159
+ console.error(`[mcp] upstream redirect ${res.status} without a Location header`);
160
+ throw new NaumuApiError(res.status, "Server error \u2014 try again shortly.");
161
+ }
162
+ const upstream = await res.text().catch(() => "Unknown error");
163
+ console.error(
164
+ `[mcp] upstream redirect ${res.status}: ${upstream.slice(0, 500)}`
165
+ );
166
+ throw new NaumuApiError(
167
+ res.status,
168
+ safeErrorMessage(res.status, upstream),
169
+ upstream
170
+ );
171
+ }
92
172
  async post(path, body) {
93
173
  const res = await fetch(`${this.baseUrl}${path}`, {
94
174
  method: "POST",
@@ -133,9 +213,6 @@ URL \u2192 tool mapping (the value after /spaces/ is the slug \u2014 resolve it
133
213
 
134
214
  Localhost URLs (http://localhost:3000/spaces/{slug}/...) follow the same shape \u2014 resolve the slug the same way. The MCP backend host is configured separately; the URL the user pastes is just for parsing structure.`;
135
215
 
136
- // ../mcp-core/src/version.ts
137
- var NAUMU_MCP_VERSION = "0.11.0";
138
-
139
216
  // ../mcp-core/src/tools/list-graphs.ts
140
217
  import { z } from "zod";
141
218
  function registerListGraphs(server2, client2) {
@@ -600,7 +677,7 @@ function registerGetNode(server2, client2) {
600
677
  {
601
678
  title: "Get Node",
602
679
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
603
- description: "Get a single node with all its properties and connections (incoming and outgoing edges).",
680
+ description: "Get a single node with all its properties, connections (incoming and outgoing edges with neighbor labels), and tied context: attached notes, conversations (threads), scheduled tasks, and the generated summary when one exists.",
604
681
  inputSchema: z11.object({
605
682
  graphId: z11.string().describe("The graph ID"),
606
683
  nodeId: z11.string().describe("The node ID")
@@ -1134,7 +1211,7 @@ function registerReadThread(server2, client2) {
1134
1211
  {
1135
1212
  title: "Read Thread",
1136
1213
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1137
- description: 'Read messages from a Naumu thread. Returns paginated history ordered newest-first; each message carries a `status` (`processing` while @Naumu is still composing, `complete` when done). Use this to pick up an answer after naumu_ask returns status "processing", or to read what naumu_delegate produced. Use `before` (timestamp ms) to page further back. Default page size 50, max 200.',
1214
+ description: 'Read messages from a Naumu thread. Returns paginated history ordered newest-first; each message carries a `status` (`processing` while @Naumu is still composing, `complete` when done). Use this to pick up an answer after naumu_ask returns status "processing", or to read what naumu_delegate produced. Use `before` (timestamp ms) to page further back. Default page size 50, max 200. To read or download a message attachment, pass its `attachments[].id` to naumu_get_attachment.',
1138
1215
  inputSchema: z21.object({
1139
1216
  threadId: z21.string().describe("The thread ID to read from."),
1140
1217
  before: z21.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
@@ -1206,15 +1283,31 @@ function registerListThreads(server2, client2) {
1206
1283
  {
1207
1284
  title: "List Threads",
1208
1285
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1209
- description: "List threads sorted by last activity (newest first), for self-discovery before deciding which to engage. With a user API key, pass `graphId` to list threads you can see in that space (resolve it via naumu_list_graphs). With a bot identity key, omit `graphId` to list threads in your own graph \u2014 each row carries an `isParticipant` flag (TRUE means you were explicitly invited and your replies fan out via webhook). Page back with `cursor` set to the oldest `lastActivityAt` from the previous page.",
1286
+ description: "List threads sorted by last activity (newest first), for self-discovery before deciding which to engage. With a user API key, pass `graphId` to list threads you can see in that space (resolve it via naumu_list_graphs). Pass `nodeId` alongside `graphId` to list only the conversations tied to that node (attached, or that created/modified it). With a bot identity key, omit `graphId` to list threads in your own graph \u2014 each row carries an `isParticipant` flag (TRUE means you were explicitly invited and your replies fan out via webhook). Page back with `cursor` set to the oldest `lastActivityAt` from the previous page.",
1210
1287
  inputSchema: z23.object({
1211
1288
  graphId: z23.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1289
+ nodeId: z23.string().optional().describe("Scope the listing to conversations tied to this node. Requires `graphId`."),
1212
1290
  cursor: z23.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1213
1291
  limit: z23.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1214
1292
  })
1215
1293
  },
1216
- async ({ graphId, cursor, limit }) => {
1294
+ async ({ graphId, nodeId, cursor, limit }) => {
1217
1295
  try {
1296
+ if (nodeId) {
1297
+ if (!graphId) {
1298
+ return {
1299
+ content: [{ type: "text", text: "Error: `nodeId` requires `graphId`." }],
1300
+ isError: true
1301
+ };
1302
+ }
1303
+ const data2 = await client2.get(
1304
+ `/api/threads?graphId=${encodeURIComponent(graphId)}&nodeId=${encodeURIComponent(nodeId)}`
1305
+ );
1306
+ const clean2 = Array.isArray(data2) ? data2.map(sanitizeThreadParticipants) : data2;
1307
+ return {
1308
+ content: [{ type: "text", text: JSON.stringify(clean2, null, 2) }]
1309
+ };
1310
+ }
1218
1311
  const params = new URLSearchParams();
1219
1312
  if (cursor !== void 0) params.set("cursor", String(cursor));
1220
1313
  if (limit !== void 0) params.set("limit", String(limit));
@@ -1580,8 +1673,80 @@ function registerPersistCanvasAttachment(server2, client2) {
1580
1673
  );
1581
1674
  }
1582
1675
 
1583
- // ../mcp-core/src/tools/add-reaction.ts
1676
+ // ../mcp-core/src/tools/get-attachment.ts
1584
1677
  import { z as z30 } from "zod";
1678
+ var DOWNLOAD_URL_TTL_SECONDS = 900;
1679
+ var MAX_INLINE_PREVIEW_BYTES = 4 * 1024 * 1024;
1680
+ function normalizeAttachmentId(input) {
1681
+ const trimmed = input.trim();
1682
+ const withoutQuery = trimmed.split(/[?#]/)[0];
1683
+ const segments = withoutQuery.split("/").filter((segment) => segment.length > 0);
1684
+ const last = segments[segments.length - 1] ?? "";
1685
+ try {
1686
+ return decodeURIComponent(last);
1687
+ } catch {
1688
+ return last;
1689
+ }
1690
+ }
1691
+ function registerGetAttachment(server2, client2) {
1692
+ server2.registerTool(
1693
+ "naumu_get_attachment",
1694
+ {
1695
+ title: "Get Attachment",
1696
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1697
+ description: "Read a chat attachment. Pass the `attachmentId` from a message's `attachments[].id` in naumu_read_thread (its `url` field works too - the id is extracted from it), and this resolves it into a short-lived download URL for the actual bytes.\n\nReturns a JSON text block with `{ attachmentId, downloadUrl, expiresInSeconds, note }`. Use `downloadUrl` EXACTLY as given:\n\u2022 GET it with no Authorization header - the URL itself is the auth, and adding one makes S3 answer 403.\n\u2022 It expires about 15 minutes after this call. Fetch it now; re-call this tool for a fresh URL rather than holding one.\n\u2022 Do not log it, quote it back to the user, or store it - it is a bearer capability for its whole lifetime.\n\nWhen the attachment is an image (or a video or PDF that has a generated poster), a downsized preview is also returned inline as an image block, so a visual attachment can often be understood without fetching anything. The preview is a thumbnail, not the original - fetch `downloadUrl` when you need full resolution or the exact file.\n\nAccess is checked the same way it is for a person: you only resolve attachments in threads you can already read.",
1698
+ inputSchema: z30.object({
1699
+ attachmentId: z30.string().min(1).describe("Attachment id, from `attachments[].id` on a message returned by naumu_read_thread. A full or relative download URL is also accepted - the id is extracted from its last path segment.")
1700
+ })
1701
+ },
1702
+ async ({ attachmentId }) => {
1703
+ try {
1704
+ const id = normalizeAttachmentId(attachmentId);
1705
+ const encoded = encodeURIComponent(id);
1706
+ const downloadUrl = await client2.getRedirectLocation(
1707
+ `/api/attachments/download/${encoded}`
1708
+ );
1709
+ const content = [];
1710
+ try {
1711
+ const preview = await client2.getBinary(
1712
+ `/api/attachments/download/${encoded}?thumbnail=preview`
1713
+ );
1714
+ if (preview.contentType.startsWith("image/") && preview.buffer.length <= MAX_INLINE_PREVIEW_BYTES) {
1715
+ content.push({
1716
+ type: "image",
1717
+ data: Buffer.from(preview.buffer).toString("base64"),
1718
+ mimeType: preview.contentType
1719
+ });
1720
+ }
1721
+ } catch {
1722
+ }
1723
+ content.push({
1724
+ type: "text",
1725
+ text: JSON.stringify(
1726
+ {
1727
+ attachmentId: id,
1728
+ downloadUrl,
1729
+ expiresInSeconds: DOWNLOAD_URL_TTL_SECONDS,
1730
+ note: "downloadUrl is presigned - fetch it WITHOUT any Authorization header. It expires in about 15 minutes; do not log or store it."
1731
+ },
1732
+ null,
1733
+ 2
1734
+ )
1735
+ });
1736
+ return { content };
1737
+ } catch (err) {
1738
+ const message = err instanceof Error ? err.message : String(err);
1739
+ return {
1740
+ content: [{ type: "text", text: `Error: ${message}` }],
1741
+ isError: true
1742
+ };
1743
+ }
1744
+ }
1745
+ );
1746
+ }
1747
+
1748
+ // ../mcp-core/src/tools/add-reaction.ts
1749
+ import { z as z31 } from "zod";
1585
1750
  function registerAddReaction(server2, client2) {
1586
1751
  server2.registerTool(
1587
1752
  "naumu_add_reaction",
@@ -1589,10 +1754,10 @@ function registerAddReaction(server2, client2) {
1589
1754
  title: "Add Reaction",
1590
1755
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1591
1756
  description: 'Add an emoji reaction to a message in a thread you are participating in; use for lightweight acknowledgement instead of posting a message. Idempotent - calling twice with the same emoji is a no-op (use `naumu_remove_reaction` to undo). Returns `{ ok, messageId, emoji, alreadyExisted, reactionCount, reactions }` so you can confirm the state without re-reading the thread; `alreadyExisted: true` means the reaction was already on the message and the call was a no-op.\n\nWhen to react vs. when to post a message:\n\u2022 React (no message) for lightweight acknowledgement (\u{1F440}, \u2705, \u{1F44D}), appreciation (\u2764\uFE0F, \u{1F64C}), laughter (\u{1F602}), or "I saw this".\n\u2022 Post a message for direct questions, clarification, important corrections, or final results - situations where words are required.\n\u2022 For long tasks: react \u{1F440} first to acknowledge, optionally post a short "On it - I\'ll report back" if the work will take >20s, do the work, then post the final result.\n\u2022 Ignore casual human banter, side-conversations someone else already answered, or anything where you would only say "ok"/"nice"/"lol".\n\nUse at most one reaction per message unless explicitly useful. Reactions are social backpressure relief, not a sparkle-confetti channel.',
1592
- inputSchema: z30.object({
1593
- threadId: z30.string().describe("Thread containing the message. You must be a participant."),
1594
- messageId: z30.string().describe("The message to react to."),
1595
- emoji: z30.string().min(1).describe('Emoji character (e.g. "\u{1F440}", "\u2705", "\u2764\uFE0F"). Custom-emoji shortcodes are NOT supported here - pass a real Unicode emoji.')
1757
+ inputSchema: z31.object({
1758
+ threadId: z31.string().describe("Thread containing the message. You must be a participant."),
1759
+ messageId: z31.string().describe("The message to react to."),
1760
+ emoji: z31.string().min(1).describe('Emoji character (e.g. "\u{1F440}", "\u2705", "\u2764\uFE0F"). Custom-emoji shortcodes are NOT supported here - pass a real Unicode emoji.')
1596
1761
  })
1597
1762
  },
1598
1763
  async ({ threadId, messageId, emoji }) => {
@@ -1616,7 +1781,7 @@ function registerAddReaction(server2, client2) {
1616
1781
  }
1617
1782
 
1618
1783
  // ../mcp-core/src/tools/remove-reaction.ts
1619
- import { z as z31 } from "zod";
1784
+ import { z as z32 } from "zod";
1620
1785
  function registerRemoveReaction(server2, client2) {
1621
1786
  server2.registerTool(
1622
1787
  "naumu_remove_reaction",
@@ -1624,10 +1789,10 @@ function registerRemoveReaction(server2, client2) {
1624
1789
  title: "Remove Reaction",
1625
1790
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1626
1791
  description: "Remove your own emoji reaction from a message; use to walk back an acknowledgement you previously added. Idempotent - calling on a reaction you never added is a no-op. Pair with `naumu_add_reaction` (e.g. you reacted \u{1F440} to start a task and want to clear it after a final result message lands). Returns `{ ok, messageId, emoji, alreadyExisted, reactionCount, reactions }` - `alreadyExisted: false` means there was nothing to remove and the call was a no-op.",
1627
- inputSchema: z31.object({
1628
- threadId: z31.string().describe("Thread containing the message. You must be a participant."),
1629
- messageId: z31.string().describe("The message to remove your reaction from."),
1630
- emoji: z31.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1792
+ inputSchema: z32.object({
1793
+ threadId: z32.string().describe("Thread containing the message. You must be a participant."),
1794
+ messageId: z32.string().describe("The message to remove your reaction from."),
1795
+ emoji: z32.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1631
1796
  })
1632
1797
  },
1633
1798
  async ({ threadId, messageId, emoji }) => {
@@ -1651,7 +1816,7 @@ function registerRemoveReaction(server2, client2) {
1651
1816
  }
1652
1817
 
1653
1818
  // ../mcp-core/src/tools/naumu-typing.ts
1654
- import { z as z32 } from "zod";
1819
+ import { z as z33 } from "zod";
1655
1820
  function registerNaumuTyping(server2, client2) {
1656
1821
  server2.registerTool(
1657
1822
  "naumu_typing",
@@ -1662,9 +1827,9 @@ function registerNaumuTyping(server2, client2) {
1662
1827
  // repeating the same state is a no-op renew, so idempotent.
1663
1828
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1664
1829
  description: 'Show or hide your "is typing\u2026" pill in a thread; use to signal that you are composing a reply. Call with `state: "start"` the moment you decide to compose a reply (before any LLM call), and the server holds the pill alive - re-broadcasting on a short interval - until you stop, post a message, or the lease cap (~5 min) fires. You do NOT need to refresh on a timer; that\'s the lease\'s job.\n\nThe pill clears automatically when:\n\u2022 you call this tool with `state: "stop"`\n\u2022 you call `naumu_post_message` for the same thread (cleared on commit)\n\u2022 the lease cap expires\n\nUse `start` whenever you start work, even if you might end up not replying - call `stop` if you decide NOT to post. Calling `start` while a lease is already active renews it (resets the cap), so a long-running run can call `start` again as a heartbeat without breaking the indicator. You must be a participant of the thread.',
1665
- inputSchema: z32.object({
1666
- threadId: z32.string().describe("The thread ID to set typing in. You must be a participant."),
1667
- state: z32.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1830
+ inputSchema: z33.object({
1831
+ threadId: z33.string().describe("The thread ID to set typing in. You must be a participant."),
1832
+ state: z33.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1668
1833
  })
1669
1834
  },
1670
1835
  async ({ threadId, state }) => {
@@ -1685,16 +1850,16 @@ function registerNaumuTyping(server2, client2) {
1685
1850
  }
1686
1851
 
1687
1852
  // ../mcp-core/src/tools/note-read.ts
1688
- import { z as z33 } from "zod";
1853
+ import { z as z34 } from "zod";
1689
1854
  function registerNoteRead(server2, client2) {
1690
1855
  server2.registerTool(
1691
1856
  "naumu_note_read",
1692
1857
  {
1693
1858
  title: "Read Note",
1694
1859
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1695
- description: "Read the current contents of a note as markdown; use before editing so you know what you're working with. `naumu_note_find_replace` and the section-based tools (`naumu_note_insert`, `naumu_note_replace_section`, `naumu_note_delete_section`) anchor on text/headings present in the live doc.",
1696
- inputSchema: z33.object({
1697
- noteId: z33.string().describe("The note (Thought) ID")
1860
+ description: "Read the current contents of a note as markdown, with its title and `connections` (the graph nodes the note is tied to); use before editing so you know what you're working with. `naumu_note_find_replace` and the section-based tools (`naumu_note_insert`, `naumu_note_replace_section`, `naumu_note_delete_section`) anchor on text/headings present in the live doc.",
1861
+ inputSchema: z34.object({
1862
+ noteId: z34.string().describe("The note (Thought) ID")
1698
1863
  })
1699
1864
  },
1700
1865
  async ({ noteId }) => {
@@ -1707,7 +1872,7 @@ function registerNoteRead(server2, client2) {
1707
1872
  }
1708
1873
 
1709
1874
  // ../mcp-core/src/tools/note-append.ts
1710
- import { z as z34 } from "zod";
1875
+ import { z as z35 } from "zod";
1711
1876
  function registerNoteAppend(server2, client2) {
1712
1877
  server2.registerTool(
1713
1878
  "naumu_note_append",
@@ -1715,9 +1880,9 @@ function registerNoteAppend(server2, client2) {
1715
1880
  title: "Append to Note",
1716
1881
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1717
1882
  description: "Append markdown blocks to the end of a note; use for additive note writing that never touches existing content. Other participants see your colored cursor while the write lands. Markdown supports headings (1-3), bold/italic/code, lists, blockquotes, code blocks, links, and tables. To embed media, write `![alt](attachment://<attachmentId>)` as a sole-line image - the id must come from `naumu_request_attachment_upload` presigned with this same `noteId`. The one syntax covers images, video, audio, and other files, dispatched by the upload's MIME type into the note's canonical media node; an unresolvable or wrong-note id fails the whole write with a 400 naming the bad id. An id already embedded in the note can be repeated safely without re-uploading.",
1718
- inputSchema: z34.object({
1719
- noteId: z34.string().describe("The note (Thought) ID to append to"),
1720
- markdown: z34.string().min(1).describe("Markdown content to append at the end of the note")
1883
+ inputSchema: z35.object({
1884
+ noteId: z35.string().describe("The note (Thought) ID to append to"),
1885
+ markdown: z35.string().min(1).describe("Markdown content to append at the end of the note")
1721
1886
  })
1722
1887
  },
1723
1888
  async ({ noteId, markdown }) => {
@@ -1730,7 +1895,7 @@ function registerNoteAppend(server2, client2) {
1730
1895
  }
1731
1896
 
1732
1897
  // ../mcp-core/src/tools/note-insert.ts
1733
- import { z as z35 } from "zod";
1898
+ import { z as z36 } from "zod";
1734
1899
  function registerNoteInsert(server2, client2) {
1735
1900
  server2.registerTool(
1736
1901
  "naumu_note_insert",
@@ -1738,10 +1903,10 @@ function registerNoteInsert(server2, client2) {
1738
1903
  title: "Insert After Heading",
1739
1904
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1740
1905
  description: "Insert markdown content into a note immediately after a named section; use to add content under a specific heading without rewriting it. The section ends at the next heading of equal-or-higher level (or end of doc). 404 if no heading matches `headingText` exactly - call `naumu_note_read` first to see the live structure. To embed media, write `![alt](attachment://<attachmentId>)` as a sole-line image - the id must come from `naumu_request_attachment_upload` presigned with this same `noteId`. The one syntax covers images, video, audio, and other files, dispatched by the upload's MIME type; an unresolvable or wrong-note id fails the whole write with a 400 naming the bad id.",
1741
- inputSchema: z35.object({
1742
- noteId: z35.string().describe("The note (Thought) ID"),
1743
- headingText: z35.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1744
- markdown: z35.string().min(1).describe("Markdown content to insert at the end of that section")
1906
+ inputSchema: z36.object({
1907
+ noteId: z36.string().describe("The note (Thought) ID"),
1908
+ headingText: z36.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1909
+ markdown: z36.string().min(1).describe("Markdown content to insert at the end of that section")
1745
1910
  })
1746
1911
  },
1747
1912
  async ({ noteId, headingText, markdown }) => {
@@ -1757,7 +1922,7 @@ function registerNoteInsert(server2, client2) {
1757
1922
  }
1758
1923
 
1759
1924
  // ../mcp-core/src/tools/note-replace-section.ts
1760
- import { z as z36 } from "zod";
1925
+ import { z as z37 } from "zod";
1761
1926
  function registerNoteReplaceSection(server2, client2) {
1762
1927
  server2.registerTool(
1763
1928
  "naumu_note_replace_section",
@@ -1765,11 +1930,11 @@ function registerNoteReplaceSection(server2, client2) {
1765
1930
  title: "Replace Section",
1766
1931
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1767
1932
  description: "Replace the body under a named heading with new markdown; use to rewrite one section of a note while leaving the rest intact. By default the heading row itself is preserved (set `keepHeading: false` to drop it too). 404 if no heading matches. To embed media, write `![alt](attachment://<attachmentId>)` as a sole-line image - the id must come from `naumu_request_attachment_upload` presigned with this same `noteId`. The one syntax covers images, video, audio, and other files, dispatched by the upload's MIME type; an unresolvable or wrong-note id fails the whole write with a 400 naming the bad id. An id already embedded elsewhere in the note can be repeated safely without re-uploading.",
1768
- inputSchema: z36.object({
1769
- noteId: z36.string().describe("The note (Thought) ID"),
1770
- headingText: z36.string().min(1).describe("Exact text of the heading anchoring the section"),
1771
- markdown: z36.string().describe("Replacement markdown for the section body"),
1772
- keepHeading: z36.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1933
+ inputSchema: z37.object({
1934
+ noteId: z37.string().describe("The note (Thought) ID"),
1935
+ headingText: z37.string().min(1).describe("Exact text of the heading anchoring the section"),
1936
+ markdown: z37.string().describe("Replacement markdown for the section body"),
1937
+ keepHeading: z37.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1773
1938
  })
1774
1939
  },
1775
1940
  async ({ noteId, headingText, markdown, keepHeading }) => {
@@ -1786,7 +1951,7 @@ function registerNoteReplaceSection(server2, client2) {
1786
1951
  }
1787
1952
 
1788
1953
  // ../mcp-core/src/tools/note-delete-section.ts
1789
- import { z as z37 } from "zod";
1954
+ import { z as z38 } from "zod";
1790
1955
  function registerNoteDeleteSection(server2, client2) {
1791
1956
  server2.registerTool(
1792
1957
  "naumu_note_delete_section",
@@ -1794,9 +1959,9 @@ function registerNoteDeleteSection(server2, client2) {
1794
1959
  title: "Delete Section",
1795
1960
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1796
1961
  description: "\u26A0 DESTRUCTIVE: remove a heading row plus its body (down to the next heading of equal-or-higher level); ONLY use when the user explicitly asks to drop a section. Anything inside that section is gone - there is no per-call undo. If you're unsure which heading they meant, call `naumu_note_read` first to see the current structure. Returns 404 if `headingText` does not exactly match any live heading.",
1797
- inputSchema: z37.object({
1798
- noteId: z37.string().describe("The note (Thought) ID"),
1799
- headingText: z37.string().min(1).describe("Exact text of the heading whose section will be deleted")
1962
+ inputSchema: z38.object({
1963
+ noteId: z38.string().describe("The note (Thought) ID"),
1964
+ headingText: z38.string().min(1).describe("Exact text of the heading whose section will be deleted")
1800
1965
  })
1801
1966
  },
1802
1967
  async ({ noteId, headingText }) => {
@@ -1811,7 +1976,7 @@ function registerNoteDeleteSection(server2, client2) {
1811
1976
  }
1812
1977
 
1813
1978
  // ../mcp-core/src/tools/note-replace.ts
1814
- import { z as z38 } from "zod";
1979
+ import { z as z39 } from "zod";
1815
1980
  function registerNoteReplace(server2, client2) {
1816
1981
  server2.registerTool(
1817
1982
  "naumu_note_replace",
@@ -1819,9 +1984,9 @@ function registerNoteReplace(server2, client2) {
1819
1984
  title: "Replace Note",
1820
1985
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1821
1986
  description: "\u26A0 DESTRUCTIVE: replace the entire note content with new markdown; ONLY use when the user explicitly asks to rewrite/replace the whole note. Any concurrent human edits made during the call are silently overwritten. For additive work prefer `naumu_note_append`. For section-level edits use `naumu_note_replace_section`. For inline tweaks use `naumu_note_find_replace`. Read with `naumu_note_read` first if you weren't the last writer. To embed media, write `![alt](attachment://<attachmentId>)` as a sole-line image - the id must come from `naumu_request_attachment_upload` presigned with this same `noteId`. The one syntax covers images, video, audio, and other files, dispatched by the upload's MIME type; an unresolvable or wrong-note id fails the whole write with a 400 naming the bad id. Ids already embedded in the note (read them back via `naumu_note_read`) can be repeated safely without re-uploading.",
1822
- inputSchema: z38.object({
1823
- noteId: z38.string().describe("The note (Thought) ID"),
1824
- markdown: z38.string().describe("New markdown content for the entire note")
1987
+ inputSchema: z39.object({
1988
+ noteId: z39.string().describe("The note (Thought) ID"),
1989
+ markdown: z39.string().describe("New markdown content for the entire note")
1825
1990
  })
1826
1991
  },
1827
1992
  async ({ noteId, markdown }) => {
@@ -1834,7 +1999,7 @@ function registerNoteReplace(server2, client2) {
1834
1999
  }
1835
2000
 
1836
2001
  // ../mcp-core/src/tools/note-find-replace.ts
1837
- import { z as z39 } from "zod";
2002
+ import { z as z40 } from "zod";
1838
2003
  function registerNoteFindReplace(server2, client2) {
1839
2004
  server2.registerTool(
1840
2005
  "naumu_note_find_replace",
@@ -1842,11 +2007,11 @@ function registerNoteFindReplace(server2, client2) {
1842
2007
  title: "Find/Replace in Note",
1843
2008
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1844
2009
  description: "Literal find/replace within a note's text content; use for mid-paragraph tweaks the section-based tools can't target. Marks (bold, italic, code, etc.) are preserved on the surrounding text. \u26A0 The match is literal-substring across every text leaf in the doc; an overly generic `find` (e.g. \" a \") can rewrite the doc unrecognizably. Pick a phrase distinctive enough to land where you mean. By default replaces every occurrence; set `all: false` for first-only. Returns `{ replacements }` so you can sanity-check the count. `replace` can embed media by containing `![alt](attachment://<attachmentId>)` as a sole-line image - the id must come from `naumu_request_attachment_upload` presigned with this same `noteId`; an unresolvable or wrong-note id fails the whole write with a 400 naming the bad id.",
1845
- inputSchema: z39.object({
1846
- noteId: z39.string().describe("The note (Thought) ID"),
1847
- find: z39.string().min(1).describe("Substring to search for. Literal - no regex."),
1848
- replace: z39.string().describe("Replacement string. May be empty to delete the match."),
1849
- all: z39.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
2010
+ inputSchema: z40.object({
2011
+ noteId: z40.string().describe("The note (Thought) ID"),
2012
+ find: z40.string().min(1).describe("Substring to search for. Literal - no regex."),
2013
+ replace: z40.string().describe("Replacement string. May be empty to delete the match."),
2014
+ all: z40.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1850
2015
  })
1851
2016
  },
1852
2017
  async ({ noteId, find, replace, all }) => {
@@ -1863,7 +2028,7 @@ function registerNoteFindReplace(server2, client2) {
1863
2028
  }
1864
2029
 
1865
2030
  // ../mcp-core/src/tools/create-note.ts
1866
- import { z as z40 } from "zod";
2031
+ import { z as z41 } from "zod";
1867
2032
  function registerCreateNote(server2, client2) {
1868
2033
  server2.registerTool(
1869
2034
  "naumu_create_note",
@@ -1871,12 +2036,12 @@ function registerCreateNote(server2, client2) {
1871
2036
  title: "Create Note",
1872
2037
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1873
2038
  description: "Create a note in a graph, optionally with its full content already in place. Pass `markdown` to create the note and its body in a single call - the preferred path for imports and for any content you already hold. Returns the new note row including its `id`; use `naumu_note_append` / `naumu_note_replace` for LATER edits, not to fill in content you could have passed here. The returned row is the note as it stood BEFORE the body write landed, so its content may read as empty - the write still succeeded, do not retry the call or re-append the body. `attachment://` refs are not accepted in create-time `markdown`: upload the file after the note exists and embed it with a note write tool. Bots can only create notes in their own graph, and a bot-created note is private (participants only) unless `sharedWithSpace` is set true. Bots cannot file notes into topics - a bot passing `topicIds` is rejected (bots hold no topic membership); use `sharedWithSpace` instead.",
1874
- inputSchema: z40.object({
1875
- graphId: z40.string().describe("The graph ID to create the note in"),
1876
- title: z40.string().optional().describe("Optional title for the note"),
1877
- markdown: z40.string().optional().describe("Full initial note content as markdown. Provide it here to create the note with its content in a single call - preferred for imports; do not restate large content through extra edit calls."),
1878
- sharedWithSpace: z40.boolean().optional().describe("Share the note with everyone in the space. Omit (or false) to keep it private to its participants."),
1879
- topicIds: z40.array(z40.string()).max(8).optional().describe("File the note into these topics (get ids from naumu_list_topics), making it visible to those topics' members. Not available to bots.")
2039
+ inputSchema: z41.object({
2040
+ graphId: z41.string().describe("The graph ID to create the note in"),
2041
+ title: z41.string().optional().describe("Optional title for the note"),
2042
+ markdown: z41.string().optional().describe("Full initial note content as markdown. Provide it here to create the note with its content in a single call - preferred for imports; do not restate large content through extra edit calls."),
2043
+ sharedWithSpace: z41.boolean().optional().describe("Share the note with everyone in the space. Omit (or false) to keep it private to its participants."),
2044
+ topicIds: z41.array(z41.string()).max(8).optional().describe("File the note into these topics (get ids from naumu_list_topics), making it visible to those topics' members. Not available to bots.")
1880
2045
  })
1881
2046
  },
1882
2047
  async ({ graphId, title, markdown, sharedWithSpace, topicIds }) => {
@@ -1889,7 +2054,7 @@ function registerCreateNote(server2, client2) {
1889
2054
  }
1890
2055
 
1891
2056
  // ../mcp-core/src/tools/list-schema-violations.ts
1892
- import { z as z41 } from "zod";
2057
+ import { z as z42 } from "zod";
1893
2058
  var DEFAULT_EXAMPLE_LIMIT = 5;
1894
2059
  var rowsForKind = (violations, kind) => {
1895
2060
  const rows = [];
@@ -1915,12 +2080,12 @@ function registerListSchemaViolations(server2, client2) {
1915
2080
  title: "List Schema Violations",
1916
2081
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1917
2082
  description: "Audit a graph against its schema. By default returns a compact summary: total counts plus, for each violation kind, its count and up to 5 example nodes (id/label/type/message) \u2014 small enough not to flood the client. Violation kinds: parent_missing (schema expects a parent edge that does not exist), parent_multiple (more than one parent edge where one is expected), parent_mismatch (parent edge has wrong target type or relation label), parent_not_backbone (an edge uses a backbone/parent relation but is not stored as a backbone edge, so the subtree stays off the hierarchy), unknown_relation (edge uses a relation not in the schema), invalid_connection_target (edge connects to a type the schema does not allow for this source), unknown_type (node carries a type no longer in the schema), disconnected (node heads a group with no backbone path to the main tree). To see every node for one kind, pass `kind` to filter; `limit` caps how many rows are returned (examples in the default summary, or full rows when `kind` is set). Use for audits, import-verification, and CI-style checks after batch writes.",
1918
- inputSchema: z41.object({
1919
- graphId: z41.string().describe("The graph ID"),
1920
- kind: z41.string().optional().describe(
2083
+ inputSchema: z42.object({
2084
+ graphId: z42.string().describe("The graph ID"),
2085
+ kind: z42.string().optional().describe(
1921
2086
  'Drill into one violation kind (e.g. "parent_not_backbone"). Returns the full list of nodes with that kind, up to `limit`, instead of the summary.'
1922
2087
  ),
1923
- limit: z41.number().int().min(1).optional().describe(
2088
+ limit: z42.number().int().min(1).optional().describe(
1924
2089
  "Max rows to return. When `kind` is set, caps the full drill-down list (default: all). Otherwise caps example nodes per kind in the summary (default: 5)."
1925
2090
  )
1926
2091
  })
@@ -1972,7 +2137,7 @@ function registerListSchemaViolations(server2, client2) {
1972
2137
  }
1973
2138
 
1974
2139
  // ../mcp-core/src/tools/list-dense-nodes.ts
1975
- import { z as z42 } from "zod";
2140
+ import { z as z43 } from "zod";
1976
2141
  function registerListDenseNodes(server2, client2) {
1977
2142
  server2.registerTool(
1978
2143
  "naumu_list_dense_nodes",
@@ -1980,10 +2145,10 @@ function registerListDenseNodes(server2, client2) {
1980
2145
  title: "List Dense Nodes",
1981
2146
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1982
2147
  description: 'Return nodes whose child count (children via parent edges; mesh cross-links don\'t count) is \u2265 minConnections, grouped by type; use for /restructure hub detection. Each row includes `same_typed_child_count` - the number of children of the SAME type as the node (the Naumu hub-pattern signal) and `connection_count` - its total children. Sort the response by `same_typed_child_count` descending and route any node with \u226510 same-typed children through a mini-hub split. Pass `nodeTypes` (comma-separated) to restrict to a subset (e.g. ["Type A","Type B"]). Cheap to call - runs a single Cypher aggregation.',
1983
- inputSchema: z42.object({
1984
- graphId: z42.string().describe("The graph ID"),
1985
- minConnections: z42.number().int().min(1).describe("Minimum number of children (parent edges; mesh cross-links excluded). Typical: 10 for hub detection, 11 to count only hubs that exceed the round-4 \u226410 threshold."),
1986
- nodeTypes: z42.array(z42.string()).optional().describe("Optional list of node types to restrict the scan to.")
2148
+ inputSchema: z43.object({
2149
+ graphId: z43.string().describe("The graph ID"),
2150
+ minConnections: z43.number().int().min(1).describe("Minimum number of children (parent edges; mesh cross-links excluded). Typical: 10 for hub detection, 11 to count only hubs that exceed the round-4 \u226410 threshold."),
2151
+ nodeTypes: z43.array(z43.string()).optional().describe("Optional list of node types to restrict the scan to.")
1987
2152
  })
1988
2153
  },
1989
2154
  async ({ graphId, minConnections, nodeTypes }) => {
@@ -2001,7 +2166,7 @@ function registerListDenseNodes(server2, client2) {
2001
2166
  }
2002
2167
 
2003
2168
  // ../mcp-core/src/tools/list-node-connections.ts
2004
- import { z as z43 } from "zod";
2169
+ import { z as z44 } from "zod";
2005
2170
  function registerListNodeConnections(server2, client2) {
2006
2171
  server2.registerTool(
2007
2172
  "naumu_list_node_connections",
@@ -2009,11 +2174,11 @@ function registerListNodeConnections(server2, client2) {
2009
2174
  title: "List Node Connections",
2010
2175
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2011
2176
  description: 'Return a single node\'s edges (non-system) with the connected node on the other side; use during /restructure to confirm mini-hub candidates and verify reparenting outcomes. Filter with `edgeType` (relation label) and `direction` ("in" | "out" | "both", default both). Response: `{ node: {id,label,type}, edges: [{relation, direction, isParent, other: {id,label,type}}] }`.',
2012
- inputSchema: z43.object({
2013
- graphId: z43.string().describe("The graph ID"),
2014
- nodeId: z43.string().describe("The node ID to inspect"),
2015
- edgeType: z43.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
2016
- direction: z43.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
2177
+ inputSchema: z44.object({
2178
+ graphId: z44.string().describe("The graph ID"),
2179
+ nodeId: z44.string().describe("The node ID to inspect"),
2180
+ edgeType: z44.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
2181
+ direction: z44.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
2017
2182
  })
2018
2183
  },
2019
2184
  async ({ graphId, nodeId, edgeType, direction }) => {
@@ -2031,7 +2196,7 @@ function registerListNodeConnections(server2, client2) {
2031
2196
  }
2032
2197
 
2033
2198
  // ../mcp-core/src/tools/reparent.ts
2034
- import { z as z44 } from "zod";
2199
+ import { z as z45 } from "zod";
2035
2200
  function registerReparent(server2, client2) {
2036
2201
  server2.registerTool(
2037
2202
  "naumu_reparent",
@@ -2039,11 +2204,11 @@ function registerReparent(server2, client2) {
2039
2204
  title: "Reparent Node",
2040
2205
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2041
2206
  description: 'Atomically swap a node\'s parent edge; use to move a child under a different parent (e.g. during /restructure to reparent children under newly-created mini-hubs). Deletes any existing `isParent: true` edges on the node and creates a new one to `newParentId` with relation `newRelation`. Preserves the node\'s id, content, attributes, and embedding - does NOT trigger embedding regeneration because only the parent edge changes. Idempotent: if the node already has the requested parent edge, response is `status: "skipped"`. Response shape: `{nodeId, oldParentId, newParentId, newRelation, status: "moved" | "skipped"}`.',
2042
- inputSchema: z44.object({
2043
- graphId: z44.string().describe("The graph ID"),
2044
- nodeId: z44.string().describe("The child node to reparent"),
2045
- newParentId: z44.string().describe("The new parent node id"),
2046
- newRelation: z44.string().describe('The new parent edge relation label (e.g. "PART_OF"). Must be valid per the schema for (child.type, relation, parent.type).')
2207
+ inputSchema: z45.object({
2208
+ graphId: z45.string().describe("The graph ID"),
2209
+ nodeId: z45.string().describe("The child node to reparent"),
2210
+ newParentId: z45.string().describe("The new parent node id"),
2211
+ newRelation: z45.string().describe('The new parent edge relation label (e.g. "PART_OF"). Must be valid per the schema for (child.type, relation, parent.type).')
2047
2212
  })
2048
2213
  },
2049
2214
  async ({ graphId, nodeId, newParentId, newRelation }) => {
@@ -2059,7 +2224,7 @@ function registerReparent(server2, client2) {
2059
2224
  }
2060
2225
 
2061
2226
  // ../mcp-core/src/tools/batch-reparent.ts
2062
- import { z as z45 } from "zod";
2227
+ import { z as z46 } from "zod";
2063
2228
  function registerBatchReparent(server2, client2) {
2064
2229
  server2.registerTool(
2065
2230
  "naumu_batch_reparent",
@@ -2067,11 +2232,11 @@ function registerBatchReparent(server2, client2) {
2067
2232
  title: "Batch Reparent Nodes",
2068
2233
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2069
2234
  description: 'Reparent 1-25 nodes onto a shared `newParentId` with the same `newRelation`; use to move a same-typed cluster under a freshly-created mini-hub in /restructure. Same semantics as `naumu_reparent` per-node: atomic swap of the isParent edge, preserves id/content/attributes/embedding, no re-embedding. Idempotent per node (already-parented nodes return `status: "skipped"`). Per-node response array: `[{nodeId, oldParentId, newParentId, status: "moved" | "skipped" | "error", error?}]`.',
2070
- inputSchema: z45.object({
2071
- graphId: z45.string().describe("The graph ID"),
2072
- newParentId: z45.string().describe("Parent node id every nodeId in the batch will be parented to"),
2073
- newRelation: z45.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
2074
- nodeIds: z45.array(z45.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
2235
+ inputSchema: z46.object({
2236
+ graphId: z46.string().describe("The graph ID"),
2237
+ newParentId: z46.string().describe("Parent node id every nodeId in the batch will be parented to"),
2238
+ newRelation: z46.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
2239
+ nodeIds: z46.array(z46.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
2075
2240
  })
2076
2241
  },
2077
2242
  async ({ graphId, newParentId, newRelation, nodeIds }) => {
@@ -2088,7 +2253,7 @@ function registerBatchReparent(server2, client2) {
2088
2253
  }
2089
2254
 
2090
2255
  // ../mcp-core/src/tools/chatgpt-search.ts
2091
- import { z as z46 } from "zod";
2256
+ import { z as z47 } from "zod";
2092
2257
 
2093
2258
  // ../mcp-core/src/public-origin.ts
2094
2259
  function publicOrigin() {
@@ -2142,8 +2307,8 @@ function registerChatgptSearch(server2, client2) {
2142
2307
  title: "Search",
2143
2308
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2144
2309
  description: "Search across all of the knowledge graphs (spaces) you can access and return the most relevant nodes. Returns `{ results: [{ id, title, url }] }`. Pass each result `id` to the `fetch` tool to read the full node. (This is the cross-space entry point for ChatGPT/Deep Research; within a single space, `naumu_search` exposes more controls.)",
2145
- inputSchema: z46.object({
2146
- query: z46.string().describe(
2310
+ inputSchema: z47.object({
2311
+ query: z47.string().describe(
2147
2312
  "A short contiguous phrase \u2014 an entity name, label, or ID. The text half matches it verbatim as a case-insensitive substring; the semantic half matches meaning."
2148
2313
  )
2149
2314
  })
@@ -2177,7 +2342,7 @@ function registerChatgptSearch(server2, client2) {
2177
2342
  }
2178
2343
 
2179
2344
  // ../mcp-core/src/tools/chatgpt-fetch.ts
2180
- import { z as z47 } from "zod";
2345
+ import { z as z48 } from "zod";
2181
2346
  var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2182
2347
  "id",
2183
2348
  "label",
@@ -2196,7 +2361,8 @@ var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2196
2361
  "updatedAt",
2197
2362
  "heatConvSignal",
2198
2363
  "heatConvComputedAt",
2199
- "heatComputedAt"
2364
+ "heatComputedAt",
2365
+ "context"
2200
2366
  ]);
2201
2367
  function parseResourceId(id) {
2202
2368
  const sep = id.indexOf(":");
@@ -2225,14 +2391,42 @@ ${attrLines.join("\n")}`);
2225
2391
  const edgeLines = [];
2226
2392
  for (const e of connections?.outgoing ?? []) {
2227
2393
  if (!e?.target) continue;
2228
- edgeLines.push(`- ${e.label ?? "related"} \u2192 ${e.target}${e.isParent ? " (parent)" : ""}`);
2394
+ const target = e.targetLabel ? `${e.targetLabel} (${e.target})` : e.target;
2395
+ edgeLines.push(`- ${e.label ?? "related"} \u2192 ${target}${e.isParent ? " (parent)" : ""}`);
2229
2396
  }
2230
2397
  for (const e of connections?.incoming ?? []) {
2231
2398
  if (!e?.source) continue;
2232
- edgeLines.push(`- ${e.source} \u2192 ${e.label ?? "related"} (incoming)`);
2399
+ const source = e.sourceLabel ? `${e.sourceLabel} (${e.source})` : e.source;
2400
+ edgeLines.push(`- ${source} \u2192 ${e.label ?? "related"} (incoming)`);
2233
2401
  }
2234
2402
  if (edgeLines.length) parts.push(`Connections:
2235
2403
  ${edgeLines.join("\n")}`);
2404
+ const ctx = node.context ?? {};
2405
+ if (ctx.notes?.length) {
2406
+ const more = (ctx.notesTotal ?? ctx.notes.length) - ctx.notes.length;
2407
+ const lines = ctx.notes.map((n) => `- ${n.title || "Untitled"} (${n.id})`);
2408
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2409
+ parts.push(`Notes:
2410
+ ${lines.join("\n")}`);
2411
+ }
2412
+ if (ctx.threads?.length) {
2413
+ const more = (ctx.threadsTotal ?? ctx.threads.length) - ctx.threads.length;
2414
+ const lines = ctx.threads.map(
2415
+ (t) => `- ${t.title || "Untitled"} (${t.id}${t.relations?.length ? ` \xB7 ${t.relations.join(", ")}` : ""})`
2416
+ );
2417
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2418
+ parts.push(`Conversations:
2419
+ ${lines.join("\n")}`);
2420
+ }
2421
+ if (ctx.scheduledTasks?.length) {
2422
+ const lines = ctx.scheduledTasks.map(
2423
+ (t) => `- ${t.title} (${t.id}${t.cron ? ` \xB7 cron ${t.cron}` : ""})`
2424
+ );
2425
+ parts.push(`Scheduled tasks:
2426
+ ${lines.join("\n")}`);
2427
+ }
2428
+ if (ctx.summary?.content) parts.push(`Summary:
2429
+ ${ctx.summary.content}`);
2236
2430
  return parts.join("\n\n");
2237
2431
  }
2238
2432
  function registerChatgptFetch(server2, client2) {
@@ -2242,8 +2436,8 @@ function registerChatgptFetch(server2, client2) {
2242
2436
  title: "Fetch",
2243
2437
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2244
2438
  description: "Fetch the full contents of a node returned by the `search` tool. Pass the result `id` verbatim (format `<graphId>:<nodeId>`). Returns `{ id, title, text, url }` where `text` is the node content plus its type, attributes, and connections.",
2245
- inputSchema: z47.object({
2246
- id: z47.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2439
+ inputSchema: z48.object({
2440
+ id: z48.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2247
2441
  })
2248
2442
  },
2249
2443
  async ({ id }) => {
@@ -2286,7 +2480,7 @@ function registerChatgptFetch(server2, client2) {
2286
2480
  }
2287
2481
 
2288
2482
  // ../mcp-core/src/tools/admission-status.ts
2289
- import { z as z48 } from "zod";
2483
+ import { z as z49 } from "zod";
2290
2484
  function registerAdmissionStatus(server2, client2) {
2291
2485
  server2.registerTool(
2292
2486
  "naumu_admission_status",
@@ -2294,8 +2488,8 @@ function registerAdmissionStatus(server2, client2) {
2294
2488
  title: "Admission Status",
2295
2489
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2296
2490
  description: "Show who can auto-join a Naumu space (graph) and who is waiting for approval: the whitelisted emails (people who join the moment they sign in with that email), the auto-join domain wildcards, and the count of pending join requests. When there are pending requests, this also returns the full list (who requested, their email, git-email hint, and message) so you can act on them with naumu_resolve_join_request. Use it during repo init to review or seed access, or whenever the user asks who has access to a space or who is asking to join.",
2297
- inputSchema: z48.object({
2298
- graphId: z48.string().describe("The space (graph) ID to inspect admission for. You must be a member of this space.")
2491
+ inputSchema: z49.object({
2492
+ graphId: z49.string().describe("The space (graph) ID to inspect admission for. You must be a member of this space.")
2299
2493
  })
2300
2494
  },
2301
2495
  async ({ graphId }) => {
@@ -2325,7 +2519,7 @@ function registerAdmissionStatus(server2, client2) {
2325
2519
  }
2326
2520
 
2327
2521
  // ../mcp-core/src/tools/whitelist-members.ts
2328
- import { z as z49 } from "zod";
2522
+ import { z as z50 } from "zod";
2329
2523
  function registerWhitelistMembers(server2, client2) {
2330
2524
  server2.registerTool(
2331
2525
  "naumu_whitelist_members",
@@ -2338,10 +2532,10 @@ function registerWhitelistMembers(server2, client2) {
2338
2532
  openWorldHint: false
2339
2533
  },
2340
2534
  description: "Whitelist emails so those people auto-join a Naumu space (graph) the moment they sign in with that email. Use this during repo init: after you scrub git history, present the curated list of collaborators to the user, and get their explicit confirmation, call this with the confirmed emails. It is silent - it sends no invite emails, it just pre-authorizes those addresses. Returns which entries were created and which were skipped (already whitelisted or already members). Set repoInit true when this call is part of the repo init flow.",
2341
- inputSchema: z49.object({
2342
- graphId: z49.string().describe("The space (graph) ID to whitelist emails for. You must be a member of this space."),
2343
- emails: z49.array(z49.string()).min(1).describe("The emails to whitelist. Each becomes an exact-match auto-join entry. Present these to the user and get confirmation before calling."),
2344
- repoInit: z49.boolean().optional().describe("Set true when this whitelist is being seeded as part of the repo init flow, so onboarding is tracked correctly.")
2535
+ inputSchema: z50.object({
2536
+ graphId: z50.string().describe("The space (graph) ID to whitelist emails for. You must be a member of this space."),
2537
+ emails: z50.array(z50.string()).min(1).describe("The emails to whitelist. Each becomes an exact-match auto-join entry. Present these to the user and get confirmation before calling."),
2538
+ repoInit: z50.boolean().optional().describe("Set true when this whitelist is being seeded as part of the repo init flow, so onboarding is tracked correctly.")
2345
2539
  })
2346
2540
  },
2347
2541
  async ({ graphId, emails, repoInit }) => {
@@ -2365,7 +2559,7 @@ function registerWhitelistMembers(server2, client2) {
2365
2559
  }
2366
2560
 
2367
2561
  // ../mcp-core/src/tools/resolve-admission.ts
2368
- import { z as z50 } from "zod";
2562
+ import { z as z51 } from "zod";
2369
2563
  function registerResolveAdmission(server2, client2) {
2370
2564
  server2.registerTool(
2371
2565
  "naumu_resolve_admission",
@@ -2378,9 +2572,9 @@ function registerResolveAdmission(server2, client2) {
2378
2572
  openWorldHint: false
2379
2573
  },
2380
2574
  description: "The call a coding agent makes right after connecting when a repo's .naumu references a space the user is not yet a member of. It evaluates whether the user can join and does it: outcome is joined-whitelist or joined-wildcard (the user is now a member - proceed), already-member (nothing to do), request-created (a join request was just filed and is awaiting a member's approval), or request-pending (a request was already open). When the response also carries reason 'seat-limit' on a request-created/request-pending outcome, the user WOULD have auto-joined via a whitelist/domain match but the space is at its seat limit - so their access is pending an admin approving them or upgrading the plan; relay that specific reason honestly, do not just say 'no match'. Pass gitEmailHint from `git config user.email` so a matching whitelist or domain rule can admit them. Relay the outcome to the user honestly: say plainly whether they joined or are waiting for approval - never imply access that is still pending.",
2381
- inputSchema: z50.object({
2382
- graphId: z50.string().describe("The space (graph) ID referenced by the repo .naumu file that the user wants to join."),
2383
- gitEmailHint: z50.string().optional().describe("The email from `git config user.email`, used to match whitelist entries and auto-join domains.")
2575
+ inputSchema: z51.object({
2576
+ graphId: z51.string().describe("The space (graph) ID referenced by the repo .naumu file that the user wants to join."),
2577
+ gitEmailHint: z51.string().optional().describe("The email from `git config user.email`, used to match whitelist entries and auto-join domains.")
2384
2578
  })
2385
2579
  },
2386
2580
  async ({ graphId, gitEmailHint }) => {
@@ -2404,7 +2598,7 @@ function registerResolveAdmission(server2, client2) {
2404
2598
  }
2405
2599
 
2406
2600
  // ../mcp-core/src/tools/resolve-join-request.ts
2407
- import { z as z51 } from "zod";
2601
+ import { z as z52 } from "zod";
2408
2602
  function registerResolveJoinRequest(server2, client2) {
2409
2603
  server2.registerTool(
2410
2604
  "naumu_resolve_join_request",
@@ -2417,10 +2611,10 @@ function registerResolveJoinRequest(server2, client2) {
2417
2611
  openWorldHint: false
2418
2612
  },
2419
2613
  description: "For a member resolving a pending join request surfaced by naumu_admission_status. Approve to add the requester to the space as a member, or deny to reject the request. Get the requestId from naumu_admission_status's pending list, and confirm the decision with the user before calling since approving grants access.",
2420
- inputSchema: z51.object({
2421
- graphId: z51.string().describe("The space (graph) ID the request is for. You must be a member of this space."),
2422
- requestId: z51.string().describe("The pending join request ID, taken from naumu_admission_status."),
2423
- action: z51.enum(["approve", "deny"]).describe("approve adds the requester as a member; deny rejects the request.")
2614
+ inputSchema: z52.object({
2615
+ graphId: z52.string().describe("The space (graph) ID the request is for. You must be a member of this space."),
2616
+ requestId: z52.string().describe("The pending join request ID, taken from naumu_admission_status."),
2617
+ action: z52.enum(["approve", "deny"]).describe("approve adds the requester as a member; deny rejects the request.")
2424
2618
  })
2425
2619
  },
2426
2620
  async ({ graphId, requestId, action }) => {
@@ -2443,6 +2637,29 @@ function registerResolveJoinRequest(server2, client2) {
2443
2637
  );
2444
2638
  }
2445
2639
 
2640
+ // ../mcp-core/src/tools/tool-attribution.ts
2641
+ function withToolAttribution(server2, client2) {
2642
+ return new Proxy(server2, {
2643
+ get(target, prop, receiver) {
2644
+ const value = Reflect.get(target, prop, receiver);
2645
+ if (prop !== "registerTool" || typeof value !== "function") return value;
2646
+ return (...args) => {
2647
+ const toolName = typeof args[0] === "string" ? args[0] : void 0;
2648
+ const handlerIndex = args.length - 1;
2649
+ const handler = args[handlerIndex];
2650
+ if (toolName && typeof handler === "function") {
2651
+ const original = handler;
2652
+ args[handlerIndex] = (...handlerArgs) => {
2653
+ client2.setPendingToolName(toolName);
2654
+ return original(...handlerArgs);
2655
+ };
2656
+ }
2657
+ return value.apply(target, args);
2658
+ };
2659
+ }
2660
+ });
2661
+ }
2662
+
2446
2663
  // ../mcp-core/src/tools/index.ts
2447
2664
  var TOOL_REGISTRARS = {
2448
2665
  naumu_list_graphs: registerListGraphs,
@@ -2470,16 +2687,18 @@ var TOOL_REGISTRARS = {
2470
2687
  naumu_whoami: registerWhoami,
2471
2688
  naumu_list_threads: registerListThreads,
2472
2689
  naumu_list_topics: registerListTopics,
2473
- // User-surface only. `POST /api/graphs/:id/topics` requires the ADMIN-only
2474
- // `space:manage-topics` permission, and bot identities always resolve to the
2475
- // editor role, so a bot calling this could only ever 403. It is therefore
2476
- // omitted from BOT_ONLY_TOOL_NAMES and has no entry in the backend
2477
- // PERMISSION_TO_MCP_TOOLS map (same treatment as the admission tools below).
2690
+ // User-surface only. Omitted from BOT_ONLY_TOOL_NAMES and from the backend
2691
+ // PERMISSION_TO_MCP_TOOLS map (same treatment as the admission tools
2692
+ // below), so bot manifests never include it. Note the backend gate alone
2693
+ // no longer keeps bots out: `space:manage-topics` is editor+ since
2694
+ // 2026-08-11 and bot identities resolve to the editor role — the manifest
2695
+ // omission is what keeps this tool off the bot surface.
2478
2696
  naumu_create_topic: registerCreateTopic,
2479
2697
  naumu_get_thread: registerGetThread,
2480
2698
  naumu_create_thread: registerCreateThread,
2481
2699
  naumu_request_attachment_upload: registerRequestAttachmentUpload,
2482
2700
  naumu_persist_canvas_attachment: registerPersistCanvasAttachment,
2701
+ naumu_get_attachment: registerGetAttachment,
2483
2702
  naumu_add_reaction: registerAddReaction,
2484
2703
  naumu_remove_reaction: registerRemoveReaction,
2485
2704
  naumu_typing: registerNaumuTyping,
@@ -2515,18 +2734,20 @@ var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS).filter(
2515
2734
  (name) => !BOT_ONLY_TOOL_NAMES.has(name)
2516
2735
  );
2517
2736
  function registerAllTools(server2, client2) {
2737
+ const attributed = withToolAttribution(server2, client2);
2518
2738
  for (const [name, registrar] of Object.entries(TOOL_REGISTRARS)) {
2519
2739
  if (BOT_ONLY_TOOL_NAMES.has(name)) continue;
2520
- registrar(server2, client2);
2740
+ registrar(attributed, client2);
2521
2741
  }
2522
2742
  }
2523
2743
  function registerNamedTools(server2, client2, toolNames) {
2744
+ const attributed = withToolAttribution(server2, client2);
2524
2745
  const registered = [];
2525
2746
  const skipped = [];
2526
2747
  for (const name of toolNames) {
2527
2748
  const registrar = TOOL_REGISTRARS[name];
2528
2749
  if (registrar) {
2529
- registrar(server2, client2);
2750
+ registrar(attributed, client2);
2530
2751
  registered.push(name);
2531
2752
  } else {
2532
2753
  skipped.push(name);
@@ -2540,16 +2761,209 @@ function registerNamedTools(server2, client2, toolNames) {
2540
2761
  console.error(`[mcp] registered ${registered.length} tool(s) for bot key`);
2541
2762
  }
2542
2763
 
2764
+ // src/doctor.ts
2765
+ var MARKERS = {
2766
+ pass: "[ok] ",
2767
+ fail: "[fail]",
2768
+ warn: "[warn]"
2769
+ };
2770
+ var BOT_KEY_PREFIX = "nmu_bot_";
2771
+ var SETTINGS_URL = "https://naumu.ai/settings";
2772
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@naumu/mcp/latest";
2773
+ var API_TIMEOUT_MS = 1e4;
2774
+ var REGISTRY_TIMEOUT_MS = 4e3;
2775
+ var errorMessage = (err) => err instanceof Error ? err.message : String(err);
2776
+ var timeoutSignal = (ms) => typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
2777
+ var formatDoctorCheck = (check) => `${MARKERS[check.status]} ${check.label}: ${check.detail}`;
2778
+ var checkApiKey = (apiKey2) => {
2779
+ if (!apiKey2) {
2780
+ return {
2781
+ label: "API key",
2782
+ status: "fail",
2783
+ detail: "NAUMU_API_KEY is missing from the environment",
2784
+ fix: `Set NAUMU_API_KEY - create a key at ${SETTINGS_URL} (Settings > Your agents > Create an API key)`
2785
+ };
2786
+ }
2787
+ const kind = apiKey2.startsWith(BOT_KEY_PREFIX) ? "bot key" : "user key";
2788
+ return {
2789
+ label: "API key",
2790
+ status: "pass",
2791
+ detail: `NAUMU_API_KEY is set (${kind}; value not printed)`
2792
+ };
2793
+ };
2794
+ var describeWhoami = (data) => {
2795
+ const row = data ?? {};
2796
+ if (row.kind === "user") {
2797
+ const email = row.email ? ` <${row.email}>` : "";
2798
+ return `authenticated as ${row.name ?? "unknown user"}${email}`;
2799
+ }
2800
+ if (row.id) {
2801
+ return `authenticated as bot identity ${row.name ?? row.id} (graph ${row.graphId ?? "unknown"})`;
2802
+ }
2803
+ return "authenticated, but the identity payload was not recognized";
2804
+ };
2805
+ var checkApiReachability = async (options) => {
2806
+ const label = "API";
2807
+ const doFetch = options.fetchImpl ?? fetch;
2808
+ const headers = { Accept: "application/json" };
2809
+ if (options.apiKey) headers.Authorization = `Bearer ${options.apiKey}`;
2810
+ let res;
2811
+ try {
2812
+ res = await doFetch(`${options.apiUrl}/api/identities/me/whoami`, {
2813
+ headers,
2814
+ signal: timeoutSignal(options.timeoutMs ?? API_TIMEOUT_MS)
2815
+ });
2816
+ } catch (err) {
2817
+ return {
2818
+ label,
2819
+ status: "fail",
2820
+ detail: `unreachable - no response from ${options.apiUrl} (${errorMessage(err)})`,
2821
+ fix: `Check your network and NAUMU_API_URL - nothing answered at ${options.apiUrl}`
2822
+ };
2823
+ }
2824
+ if (res.status === 401 || res.status === 403) {
2825
+ if (!options.apiKey) {
2826
+ return {
2827
+ label,
2828
+ status: "warn",
2829
+ detail: `reachable (${options.apiUrl} answered HTTP ${res.status}), but there is no NAUMU_API_KEY to verify`
2830
+ };
2831
+ }
2832
+ return {
2833
+ label,
2834
+ status: "fail",
2835
+ detail: `reachable, but ${options.apiUrl} rejected the key (HTTP ${res.status})`,
2836
+ fix: `NAUMU_API_KEY is invalid or revoked - create a new key at ${SETTINGS_URL} and update your MCP config`
2837
+ };
2838
+ }
2839
+ if (!res.ok) {
2840
+ return {
2841
+ label,
2842
+ status: "fail",
2843
+ detail: `reachable, but ${options.apiUrl} answered HTTP ${res.status}`,
2844
+ fix: "The Naumu API returned an unexpected status - retry shortly, or check NAUMU_API_URL points at the front-end origin"
2845
+ };
2846
+ }
2847
+ try {
2848
+ const data = await res.json();
2849
+ return { label, status: "pass", detail: `reachable at ${options.apiUrl}; ${describeWhoami(data)}` };
2850
+ } catch (err) {
2851
+ return {
2852
+ label,
2853
+ status: "fail",
2854
+ detail: `reachable, but the response from ${options.apiUrl} was not JSON (${errorMessage(err)})`,
2855
+ fix: "NAUMU_API_URL does not look like a Naumu API - point it at https://naumu.ai"
2856
+ };
2857
+ }
2858
+ };
2859
+ var compareVersions = (a, b) => {
2860
+ const parse = (value) => value.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
2861
+ const left = parse(a);
2862
+ const right = parse(b);
2863
+ for (let i = 0; i < 3; i += 1) {
2864
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
2865
+ if (diff !== 0) return diff < 0 ? -1 : 1;
2866
+ }
2867
+ return 0;
2868
+ };
2869
+ var checkVersionFreshness = async (options = {}) => {
2870
+ const label = "Version";
2871
+ const current = options.currentVersion ?? NAUMU_MCP_VERSION;
2872
+ const doFetch = options.fetchImpl ?? fetch;
2873
+ let latest;
2874
+ try {
2875
+ const res = await doFetch(REGISTRY_LATEST_URL, {
2876
+ headers: { Accept: "application/json" },
2877
+ signal: timeoutSignal(options.timeoutMs ?? REGISTRY_TIMEOUT_MS)
2878
+ });
2879
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2880
+ const body = await res.json();
2881
+ latest = body?.version;
2882
+ } catch (err) {
2883
+ return {
2884
+ label,
2885
+ status: "warn",
2886
+ detail: `running ${current}; could not check the npm registry for the latest version (${errorMessage(err)})`
2887
+ };
2888
+ }
2889
+ if (!latest) {
2890
+ return {
2891
+ label,
2892
+ status: "warn",
2893
+ detail: `running ${current}; could not check the npm registry for the latest version (no version field in the response)`
2894
+ };
2895
+ }
2896
+ const comparison = compareVersions(current, latest);
2897
+ if (comparison < 0) {
2898
+ return {
2899
+ label,
2900
+ status: "fail",
2901
+ detail: `running ${current}, but npm latest is ${latest}`,
2902
+ fix: 'Update @naumu/mcp - run "npm cache clean --force" and reinstall (globally installed: "npm i -g @naumu/mcp@latest"; npx users: pin "@naumu/mcp@latest")'
2903
+ };
2904
+ }
2905
+ if (comparison > 0) {
2906
+ return { label, status: "pass", detail: `running ${current}, ahead of npm latest ${latest} (unpublished build)` };
2907
+ }
2908
+ return { label, status: "pass", detail: `running ${current} (npm latest)` };
2909
+ };
2910
+ var runDoctorChecks = async (options) => {
2911
+ const checks = [];
2912
+ checks.push(checkApiKey(options.apiKey));
2913
+ checks.push(
2914
+ await checkApiReachability({
2915
+ apiUrl: options.apiUrl,
2916
+ apiKey: options.apiKey,
2917
+ fetchImpl: options.fetchImpl
2918
+ })
2919
+ );
2920
+ checks.push(
2921
+ await checkVersionFreshness({
2922
+ currentVersion: options.currentVersion,
2923
+ fetchImpl: options.fetchImpl
2924
+ })
2925
+ );
2926
+ return checks;
2927
+ };
2928
+ var renderDoctorReport = (checks, context) => {
2929
+ const version = context.version ?? NAUMU_MCP_VERSION;
2930
+ const lines = [
2931
+ `naumu-mcp doctor (v${version}, API ${context.apiUrl})`,
2932
+ "",
2933
+ ...checks.map(formatDoctorCheck),
2934
+ ""
2935
+ ];
2936
+ const failures = checks.filter((check) => check.status === "fail");
2937
+ if (failures.length === 0) {
2938
+ lines.push("Verdict: all checks passed - the MCP server should start normally.");
2939
+ return { text: lines.join("\n"), exitCode: 0 };
2940
+ }
2941
+ const plural = failures.length === 1 ? "check" : "checks";
2942
+ lines.push(`Verdict: ${failures.length} ${plural} failed - the MCP server will not work until this is fixed.`);
2943
+ const fix = failures.find((check) => check.fix)?.fix;
2944
+ if (fix) lines.push(`Most likely fix: ${fix}`);
2945
+ return { text: lines.join("\n"), exitCode: 1 };
2946
+ };
2947
+ var runDoctorCli = async (options) => {
2948
+ const checks = await runDoctorChecks({ apiUrl: options.apiUrl, apiKey: options.apiKey });
2949
+ const report = renderDoctorReport(checks, { apiUrl: options.apiUrl });
2950
+ console.log(report.text);
2951
+ return report.exitCode;
2952
+ };
2953
+
2543
2954
  // src/index.ts
2544
2955
  var DEFAULT_API_URL = "https://naumu.ai";
2545
- var BOT_KEY_PREFIX = "nmu_bot_";
2956
+ var BOT_KEY_PREFIX2 = "nmu_bot_";
2546
2957
  var apiUrl = (process.env.NAUMU_API_URL || DEFAULT_API_URL).replace(/\/api\/?$/, "");
2547
2958
  var apiKey = process.env.NAUMU_API_KEY;
2959
+ if (process.argv[2] === "doctor") {
2960
+ process.exit(await runDoctorCli({ apiUrl, apiKey }));
2961
+ }
2548
2962
  if (!apiKey) {
2549
2963
  console.error("Missing required environment variable: NAUMU_API_KEY");
2550
2964
  process.exit(1);
2551
2965
  }
2552
- var client = new NaumuClient(apiUrl, apiKey);
2966
+ var client = new NaumuClient(apiUrl, apiKey, { reportMcpClientHeaders: true });
2553
2967
  var server = new McpServer(
2554
2968
  {
2555
2969
  name: "naumu",
@@ -2559,7 +2973,7 @@ var server = new McpServer(
2559
2973
  instructions: NAUMU_INSTRUCTIONS
2560
2974
  }
2561
2975
  );
2562
- var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX);
2976
+ var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX2);
2563
2977
  if (isBotKey) {
2564
2978
  try {
2565
2979
  const manifest = await client.get("/api/identities/me/mcp-tool-manifest");
@@ -2578,5 +2992,9 @@ if (isBotKey) {
2578
2992
  `[mcp] connected to ${apiUrl} as user key (${process.env.NAUMU_IDENTITY_ID ?? "no identity id set"})`
2579
2993
  );
2580
2994
  }
2995
+ server.server.oninitialized = () => {
2996
+ const clientInfo = server.server.getClientVersion();
2997
+ client.setMcpClientInfo(clientInfo ? { name: clientInfo.name, version: clientInfo.version } : void 0);
2998
+ };
2581
2999
  var transport = new StdioServerTransport();
2582
3000
  await server.connect(transport);