@naumu/mcp 0.11.0 → 0.11.1

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 (2) hide show
  1. package/dist/index.js +293 -114
  2. package/package.json +2 -2
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.11.1";
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) {
@@ -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."),
@@ -1580,8 +1657,80 @@ function registerPersistCanvasAttachment(server2, client2) {
1580
1657
  );
1581
1658
  }
1582
1659
 
1583
- // ../mcp-core/src/tools/add-reaction.ts
1660
+ // ../mcp-core/src/tools/get-attachment.ts
1584
1661
  import { z as z30 } from "zod";
1662
+ var DOWNLOAD_URL_TTL_SECONDS = 900;
1663
+ var MAX_INLINE_PREVIEW_BYTES = 4 * 1024 * 1024;
1664
+ function normalizeAttachmentId(input) {
1665
+ const trimmed = input.trim();
1666
+ const withoutQuery = trimmed.split(/[?#]/)[0];
1667
+ const segments = withoutQuery.split("/").filter((segment) => segment.length > 0);
1668
+ const last = segments[segments.length - 1] ?? "";
1669
+ try {
1670
+ return decodeURIComponent(last);
1671
+ } catch {
1672
+ return last;
1673
+ }
1674
+ }
1675
+ function registerGetAttachment(server2, client2) {
1676
+ server2.registerTool(
1677
+ "naumu_get_attachment",
1678
+ {
1679
+ title: "Get Attachment",
1680
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1681
+ 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.",
1682
+ inputSchema: z30.object({
1683
+ 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.")
1684
+ })
1685
+ },
1686
+ async ({ attachmentId }) => {
1687
+ try {
1688
+ const id = normalizeAttachmentId(attachmentId);
1689
+ const encoded = encodeURIComponent(id);
1690
+ const downloadUrl = await client2.getRedirectLocation(
1691
+ `/api/attachments/download/${encoded}`
1692
+ );
1693
+ const content = [];
1694
+ try {
1695
+ const preview = await client2.getBinary(
1696
+ `/api/attachments/download/${encoded}?thumbnail=preview`
1697
+ );
1698
+ if (preview.contentType.startsWith("image/") && preview.buffer.length <= MAX_INLINE_PREVIEW_BYTES) {
1699
+ content.push({
1700
+ type: "image",
1701
+ data: Buffer.from(preview.buffer).toString("base64"),
1702
+ mimeType: preview.contentType
1703
+ });
1704
+ }
1705
+ } catch {
1706
+ }
1707
+ content.push({
1708
+ type: "text",
1709
+ text: JSON.stringify(
1710
+ {
1711
+ attachmentId: id,
1712
+ downloadUrl,
1713
+ expiresInSeconds: DOWNLOAD_URL_TTL_SECONDS,
1714
+ note: "downloadUrl is presigned - fetch it WITHOUT any Authorization header. It expires in about 15 minutes; do not log or store it."
1715
+ },
1716
+ null,
1717
+ 2
1718
+ )
1719
+ });
1720
+ return { content };
1721
+ } catch (err) {
1722
+ const message = err instanceof Error ? err.message : String(err);
1723
+ return {
1724
+ content: [{ type: "text", text: `Error: ${message}` }],
1725
+ isError: true
1726
+ };
1727
+ }
1728
+ }
1729
+ );
1730
+ }
1731
+
1732
+ // ../mcp-core/src/tools/add-reaction.ts
1733
+ import { z as z31 } from "zod";
1585
1734
  function registerAddReaction(server2, client2) {
1586
1735
  server2.registerTool(
1587
1736
  "naumu_add_reaction",
@@ -1589,10 +1738,10 @@ function registerAddReaction(server2, client2) {
1589
1738
  title: "Add Reaction",
1590
1739
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1591
1740
  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.')
1741
+ inputSchema: z31.object({
1742
+ threadId: z31.string().describe("Thread containing the message. You must be a participant."),
1743
+ messageId: z31.string().describe("The message to react to."),
1744
+ 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
1745
  })
1597
1746
  },
1598
1747
  async ({ threadId, messageId, emoji }) => {
@@ -1616,7 +1765,7 @@ function registerAddReaction(server2, client2) {
1616
1765
  }
1617
1766
 
1618
1767
  // ../mcp-core/src/tools/remove-reaction.ts
1619
- import { z as z31 } from "zod";
1768
+ import { z as z32 } from "zod";
1620
1769
  function registerRemoveReaction(server2, client2) {
1621
1770
  server2.registerTool(
1622
1771
  "naumu_remove_reaction",
@@ -1624,10 +1773,10 @@ function registerRemoveReaction(server2, client2) {
1624
1773
  title: "Remove Reaction",
1625
1774
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1626
1775
  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).")
1776
+ inputSchema: z32.object({
1777
+ threadId: z32.string().describe("Thread containing the message. You must be a participant."),
1778
+ messageId: z32.string().describe("The message to remove your reaction from."),
1779
+ emoji: z32.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1631
1780
  })
1632
1781
  },
1633
1782
  async ({ threadId, messageId, emoji }) => {
@@ -1651,7 +1800,7 @@ function registerRemoveReaction(server2, client2) {
1651
1800
  }
1652
1801
 
1653
1802
  // ../mcp-core/src/tools/naumu-typing.ts
1654
- import { z as z32 } from "zod";
1803
+ import { z as z33 } from "zod";
1655
1804
  function registerNaumuTyping(server2, client2) {
1656
1805
  server2.registerTool(
1657
1806
  "naumu_typing",
@@ -1662,9 +1811,9 @@ function registerNaumuTyping(server2, client2) {
1662
1811
  // repeating the same state is a no-op renew, so idempotent.
1663
1812
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1664
1813
  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.')
1814
+ inputSchema: z33.object({
1815
+ threadId: z33.string().describe("The thread ID to set typing in. You must be a participant."),
1816
+ state: z33.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1668
1817
  })
1669
1818
  },
1670
1819
  async ({ threadId, state }) => {
@@ -1685,7 +1834,7 @@ function registerNaumuTyping(server2, client2) {
1685
1834
  }
1686
1835
 
1687
1836
  // ../mcp-core/src/tools/note-read.ts
1688
- import { z as z33 } from "zod";
1837
+ import { z as z34 } from "zod";
1689
1838
  function registerNoteRead(server2, client2) {
1690
1839
  server2.registerTool(
1691
1840
  "naumu_note_read",
@@ -1693,8 +1842,8 @@ function registerNoteRead(server2, client2) {
1693
1842
  title: "Read Note",
1694
1843
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1695
1844
  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")
1845
+ inputSchema: z34.object({
1846
+ noteId: z34.string().describe("The note (Thought) ID")
1698
1847
  })
1699
1848
  },
1700
1849
  async ({ noteId }) => {
@@ -1707,7 +1856,7 @@ function registerNoteRead(server2, client2) {
1707
1856
  }
1708
1857
 
1709
1858
  // ../mcp-core/src/tools/note-append.ts
1710
- import { z as z34 } from "zod";
1859
+ import { z as z35 } from "zod";
1711
1860
  function registerNoteAppend(server2, client2) {
1712
1861
  server2.registerTool(
1713
1862
  "naumu_note_append",
@@ -1715,9 +1864,9 @@ function registerNoteAppend(server2, client2) {
1715
1864
  title: "Append to Note",
1716
1865
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1717
1866
  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")
1867
+ inputSchema: z35.object({
1868
+ noteId: z35.string().describe("The note (Thought) ID to append to"),
1869
+ markdown: z35.string().min(1).describe("Markdown content to append at the end of the note")
1721
1870
  })
1722
1871
  },
1723
1872
  async ({ noteId, markdown }) => {
@@ -1730,7 +1879,7 @@ function registerNoteAppend(server2, client2) {
1730
1879
  }
1731
1880
 
1732
1881
  // ../mcp-core/src/tools/note-insert.ts
1733
- import { z as z35 } from "zod";
1882
+ import { z as z36 } from "zod";
1734
1883
  function registerNoteInsert(server2, client2) {
1735
1884
  server2.registerTool(
1736
1885
  "naumu_note_insert",
@@ -1738,10 +1887,10 @@ function registerNoteInsert(server2, client2) {
1738
1887
  title: "Insert After Heading",
1739
1888
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1740
1889
  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")
1890
+ inputSchema: z36.object({
1891
+ noteId: z36.string().describe("The note (Thought) ID"),
1892
+ headingText: z36.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1893
+ markdown: z36.string().min(1).describe("Markdown content to insert at the end of that section")
1745
1894
  })
1746
1895
  },
1747
1896
  async ({ noteId, headingText, markdown }) => {
@@ -1757,7 +1906,7 @@ function registerNoteInsert(server2, client2) {
1757
1906
  }
1758
1907
 
1759
1908
  // ../mcp-core/src/tools/note-replace-section.ts
1760
- import { z as z36 } from "zod";
1909
+ import { z as z37 } from "zod";
1761
1910
  function registerNoteReplaceSection(server2, client2) {
1762
1911
  server2.registerTool(
1763
1912
  "naumu_note_replace_section",
@@ -1765,11 +1914,11 @@ function registerNoteReplaceSection(server2, client2) {
1765
1914
  title: "Replace Section",
1766
1915
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1767
1916
  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.")
1917
+ inputSchema: z37.object({
1918
+ noteId: z37.string().describe("The note (Thought) ID"),
1919
+ headingText: z37.string().min(1).describe("Exact text of the heading anchoring the section"),
1920
+ markdown: z37.string().describe("Replacement markdown for the section body"),
1921
+ keepHeading: z37.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1773
1922
  })
1774
1923
  },
1775
1924
  async ({ noteId, headingText, markdown, keepHeading }) => {
@@ -1786,7 +1935,7 @@ function registerNoteReplaceSection(server2, client2) {
1786
1935
  }
1787
1936
 
1788
1937
  // ../mcp-core/src/tools/note-delete-section.ts
1789
- import { z as z37 } from "zod";
1938
+ import { z as z38 } from "zod";
1790
1939
  function registerNoteDeleteSection(server2, client2) {
1791
1940
  server2.registerTool(
1792
1941
  "naumu_note_delete_section",
@@ -1794,9 +1943,9 @@ function registerNoteDeleteSection(server2, client2) {
1794
1943
  title: "Delete Section",
1795
1944
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1796
1945
  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")
1946
+ inputSchema: z38.object({
1947
+ noteId: z38.string().describe("The note (Thought) ID"),
1948
+ headingText: z38.string().min(1).describe("Exact text of the heading whose section will be deleted")
1800
1949
  })
1801
1950
  },
1802
1951
  async ({ noteId, headingText }) => {
@@ -1811,7 +1960,7 @@ function registerNoteDeleteSection(server2, client2) {
1811
1960
  }
1812
1961
 
1813
1962
  // ../mcp-core/src/tools/note-replace.ts
1814
- import { z as z38 } from "zod";
1963
+ import { z as z39 } from "zod";
1815
1964
  function registerNoteReplace(server2, client2) {
1816
1965
  server2.registerTool(
1817
1966
  "naumu_note_replace",
@@ -1819,9 +1968,9 @@ function registerNoteReplace(server2, client2) {
1819
1968
  title: "Replace Note",
1820
1969
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1821
1970
  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")
1971
+ inputSchema: z39.object({
1972
+ noteId: z39.string().describe("The note (Thought) ID"),
1973
+ markdown: z39.string().describe("New markdown content for the entire note")
1825
1974
  })
1826
1975
  },
1827
1976
  async ({ noteId, markdown }) => {
@@ -1834,7 +1983,7 @@ function registerNoteReplace(server2, client2) {
1834
1983
  }
1835
1984
 
1836
1985
  // ../mcp-core/src/tools/note-find-replace.ts
1837
- import { z as z39 } from "zod";
1986
+ import { z as z40 } from "zod";
1838
1987
  function registerNoteFindReplace(server2, client2) {
1839
1988
  server2.registerTool(
1840
1989
  "naumu_note_find_replace",
@@ -1842,11 +1991,11 @@ function registerNoteFindReplace(server2, client2) {
1842
1991
  title: "Find/Replace in Note",
1843
1992
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1844
1993
  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.")
1994
+ inputSchema: z40.object({
1995
+ noteId: z40.string().describe("The note (Thought) ID"),
1996
+ find: z40.string().min(1).describe("Substring to search for. Literal - no regex."),
1997
+ replace: z40.string().describe("Replacement string. May be empty to delete the match."),
1998
+ all: z40.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1850
1999
  })
1851
2000
  },
1852
2001
  async ({ noteId, find, replace, all }) => {
@@ -1863,7 +2012,7 @@ function registerNoteFindReplace(server2, client2) {
1863
2012
  }
1864
2013
 
1865
2014
  // ../mcp-core/src/tools/create-note.ts
1866
- import { z as z40 } from "zod";
2015
+ import { z as z41 } from "zod";
1867
2016
  function registerCreateNote(server2, client2) {
1868
2017
  server2.registerTool(
1869
2018
  "naumu_create_note",
@@ -1871,12 +2020,12 @@ function registerCreateNote(server2, client2) {
1871
2020
  title: "Create Note",
1872
2021
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1873
2022
  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.")
2023
+ inputSchema: z41.object({
2024
+ graphId: z41.string().describe("The graph ID to create the note in"),
2025
+ title: z41.string().optional().describe("Optional title for the note"),
2026
+ 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."),
2027
+ sharedWithSpace: z41.boolean().optional().describe("Share the note with everyone in the space. Omit (or false) to keep it private to its participants."),
2028
+ 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
2029
  })
1881
2030
  },
1882
2031
  async ({ graphId, title, markdown, sharedWithSpace, topicIds }) => {
@@ -1889,7 +2038,7 @@ function registerCreateNote(server2, client2) {
1889
2038
  }
1890
2039
 
1891
2040
  // ../mcp-core/src/tools/list-schema-violations.ts
1892
- import { z as z41 } from "zod";
2041
+ import { z as z42 } from "zod";
1893
2042
  var DEFAULT_EXAMPLE_LIMIT = 5;
1894
2043
  var rowsForKind = (violations, kind) => {
1895
2044
  const rows = [];
@@ -1915,12 +2064,12 @@ function registerListSchemaViolations(server2, client2) {
1915
2064
  title: "List Schema Violations",
1916
2065
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1917
2066
  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(
2067
+ inputSchema: z42.object({
2068
+ graphId: z42.string().describe("The graph ID"),
2069
+ kind: z42.string().optional().describe(
1921
2070
  '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
2071
  ),
1923
- limit: z41.number().int().min(1).optional().describe(
2072
+ limit: z42.number().int().min(1).optional().describe(
1924
2073
  "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
2074
  )
1926
2075
  })
@@ -1972,7 +2121,7 @@ function registerListSchemaViolations(server2, client2) {
1972
2121
  }
1973
2122
 
1974
2123
  // ../mcp-core/src/tools/list-dense-nodes.ts
1975
- import { z as z42 } from "zod";
2124
+ import { z as z43 } from "zod";
1976
2125
  function registerListDenseNodes(server2, client2) {
1977
2126
  server2.registerTool(
1978
2127
  "naumu_list_dense_nodes",
@@ -1980,10 +2129,10 @@ function registerListDenseNodes(server2, client2) {
1980
2129
  title: "List Dense Nodes",
1981
2130
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1982
2131
  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.")
2132
+ inputSchema: z43.object({
2133
+ graphId: z43.string().describe("The graph ID"),
2134
+ 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."),
2135
+ nodeTypes: z43.array(z43.string()).optional().describe("Optional list of node types to restrict the scan to.")
1987
2136
  })
1988
2137
  },
1989
2138
  async ({ graphId, minConnections, nodeTypes }) => {
@@ -2001,7 +2150,7 @@ function registerListDenseNodes(server2, client2) {
2001
2150
  }
2002
2151
 
2003
2152
  // ../mcp-core/src/tools/list-node-connections.ts
2004
- import { z as z43 } from "zod";
2153
+ import { z as z44 } from "zod";
2005
2154
  function registerListNodeConnections(server2, client2) {
2006
2155
  server2.registerTool(
2007
2156
  "naumu_list_node_connections",
@@ -2009,11 +2158,11 @@ function registerListNodeConnections(server2, client2) {
2009
2158
  title: "List Node Connections",
2010
2159
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2011
2160
  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).')
2161
+ inputSchema: z44.object({
2162
+ graphId: z44.string().describe("The graph ID"),
2163
+ nodeId: z44.string().describe("The node ID to inspect"),
2164
+ edgeType: z44.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
2165
+ direction: z44.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
2017
2166
  })
2018
2167
  },
2019
2168
  async ({ graphId, nodeId, edgeType, direction }) => {
@@ -2031,7 +2180,7 @@ function registerListNodeConnections(server2, client2) {
2031
2180
  }
2032
2181
 
2033
2182
  // ../mcp-core/src/tools/reparent.ts
2034
- import { z as z44 } from "zod";
2183
+ import { z as z45 } from "zod";
2035
2184
  function registerReparent(server2, client2) {
2036
2185
  server2.registerTool(
2037
2186
  "naumu_reparent",
@@ -2039,11 +2188,11 @@ function registerReparent(server2, client2) {
2039
2188
  title: "Reparent Node",
2040
2189
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2041
2190
  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).')
2191
+ inputSchema: z45.object({
2192
+ graphId: z45.string().describe("The graph ID"),
2193
+ nodeId: z45.string().describe("The child node to reparent"),
2194
+ newParentId: z45.string().describe("The new parent node id"),
2195
+ 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
2196
  })
2048
2197
  },
2049
2198
  async ({ graphId, nodeId, newParentId, newRelation }) => {
@@ -2059,7 +2208,7 @@ function registerReparent(server2, client2) {
2059
2208
  }
2060
2209
 
2061
2210
  // ../mcp-core/src/tools/batch-reparent.ts
2062
- import { z as z45 } from "zod";
2211
+ import { z as z46 } from "zod";
2063
2212
  function registerBatchReparent(server2, client2) {
2064
2213
  server2.registerTool(
2065
2214
  "naumu_batch_reparent",
@@ -2067,11 +2216,11 @@ function registerBatchReparent(server2, client2) {
2067
2216
  title: "Batch Reparent Nodes",
2068
2217
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2069
2218
  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`")
2219
+ inputSchema: z46.object({
2220
+ graphId: z46.string().describe("The graph ID"),
2221
+ newParentId: z46.string().describe("Parent node id every nodeId in the batch will be parented to"),
2222
+ newRelation: z46.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
2223
+ nodeIds: z46.array(z46.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
2075
2224
  })
2076
2225
  },
2077
2226
  async ({ graphId, newParentId, newRelation, nodeIds }) => {
@@ -2088,7 +2237,7 @@ function registerBatchReparent(server2, client2) {
2088
2237
  }
2089
2238
 
2090
2239
  // ../mcp-core/src/tools/chatgpt-search.ts
2091
- import { z as z46 } from "zod";
2240
+ import { z as z47 } from "zod";
2092
2241
 
2093
2242
  // ../mcp-core/src/public-origin.ts
2094
2243
  function publicOrigin() {
@@ -2142,8 +2291,8 @@ function registerChatgptSearch(server2, client2) {
2142
2291
  title: "Search",
2143
2292
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2144
2293
  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(
2294
+ inputSchema: z47.object({
2295
+ query: z47.string().describe(
2147
2296
  "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
2297
  )
2149
2298
  })
@@ -2177,7 +2326,7 @@ function registerChatgptSearch(server2, client2) {
2177
2326
  }
2178
2327
 
2179
2328
  // ../mcp-core/src/tools/chatgpt-fetch.ts
2180
- import { z as z47 } from "zod";
2329
+ import { z as z48 } from "zod";
2181
2330
  var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2182
2331
  "id",
2183
2332
  "label",
@@ -2242,8 +2391,8 @@ function registerChatgptFetch(server2, client2) {
2242
2391
  title: "Fetch",
2243
2392
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2244
2393
  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>`.")
2394
+ inputSchema: z48.object({
2395
+ id: z48.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2247
2396
  })
2248
2397
  },
2249
2398
  async ({ id }) => {
@@ -2286,7 +2435,7 @@ function registerChatgptFetch(server2, client2) {
2286
2435
  }
2287
2436
 
2288
2437
  // ../mcp-core/src/tools/admission-status.ts
2289
- import { z as z48 } from "zod";
2438
+ import { z as z49 } from "zod";
2290
2439
  function registerAdmissionStatus(server2, client2) {
2291
2440
  server2.registerTool(
2292
2441
  "naumu_admission_status",
@@ -2294,8 +2443,8 @@ function registerAdmissionStatus(server2, client2) {
2294
2443
  title: "Admission Status",
2295
2444
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2296
2445
  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.")
2446
+ inputSchema: z49.object({
2447
+ graphId: z49.string().describe("The space (graph) ID to inspect admission for. You must be a member of this space.")
2299
2448
  })
2300
2449
  },
2301
2450
  async ({ graphId }) => {
@@ -2325,7 +2474,7 @@ function registerAdmissionStatus(server2, client2) {
2325
2474
  }
2326
2475
 
2327
2476
  // ../mcp-core/src/tools/whitelist-members.ts
2328
- import { z as z49 } from "zod";
2477
+ import { z as z50 } from "zod";
2329
2478
  function registerWhitelistMembers(server2, client2) {
2330
2479
  server2.registerTool(
2331
2480
  "naumu_whitelist_members",
@@ -2338,10 +2487,10 @@ function registerWhitelistMembers(server2, client2) {
2338
2487
  openWorldHint: false
2339
2488
  },
2340
2489
  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.")
2490
+ inputSchema: z50.object({
2491
+ graphId: z50.string().describe("The space (graph) ID to whitelist emails for. You must be a member of this space."),
2492
+ 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."),
2493
+ 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
2494
  })
2346
2495
  },
2347
2496
  async ({ graphId, emails, repoInit }) => {
@@ -2365,7 +2514,7 @@ function registerWhitelistMembers(server2, client2) {
2365
2514
  }
2366
2515
 
2367
2516
  // ../mcp-core/src/tools/resolve-admission.ts
2368
- import { z as z50 } from "zod";
2517
+ import { z as z51 } from "zod";
2369
2518
  function registerResolveAdmission(server2, client2) {
2370
2519
  server2.registerTool(
2371
2520
  "naumu_resolve_admission",
@@ -2378,9 +2527,9 @@ function registerResolveAdmission(server2, client2) {
2378
2527
  openWorldHint: false
2379
2528
  },
2380
2529
  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.")
2530
+ inputSchema: z51.object({
2531
+ graphId: z51.string().describe("The space (graph) ID referenced by the repo .naumu file that the user wants to join."),
2532
+ gitEmailHint: z51.string().optional().describe("The email from `git config user.email`, used to match whitelist entries and auto-join domains.")
2384
2533
  })
2385
2534
  },
2386
2535
  async ({ graphId, gitEmailHint }) => {
@@ -2404,7 +2553,7 @@ function registerResolveAdmission(server2, client2) {
2404
2553
  }
2405
2554
 
2406
2555
  // ../mcp-core/src/tools/resolve-join-request.ts
2407
- import { z as z51 } from "zod";
2556
+ import { z as z52 } from "zod";
2408
2557
  function registerResolveJoinRequest(server2, client2) {
2409
2558
  server2.registerTool(
2410
2559
  "naumu_resolve_join_request",
@@ -2417,10 +2566,10 @@ function registerResolveJoinRequest(server2, client2) {
2417
2566
  openWorldHint: false
2418
2567
  },
2419
2568
  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.")
2569
+ inputSchema: z52.object({
2570
+ graphId: z52.string().describe("The space (graph) ID the request is for. You must be a member of this space."),
2571
+ requestId: z52.string().describe("The pending join request ID, taken from naumu_admission_status."),
2572
+ action: z52.enum(["approve", "deny"]).describe("approve adds the requester as a member; deny rejects the request.")
2424
2573
  })
2425
2574
  },
2426
2575
  async ({ graphId, requestId, action }) => {
@@ -2443,6 +2592,29 @@ function registerResolveJoinRequest(server2, client2) {
2443
2592
  );
2444
2593
  }
2445
2594
 
2595
+ // ../mcp-core/src/tools/tool-attribution.ts
2596
+ function withToolAttribution(server2, client2) {
2597
+ return new Proxy(server2, {
2598
+ get(target, prop, receiver) {
2599
+ const value = Reflect.get(target, prop, receiver);
2600
+ if (prop !== "registerTool" || typeof value !== "function") return value;
2601
+ return (...args) => {
2602
+ const toolName = typeof args[0] === "string" ? args[0] : void 0;
2603
+ const handlerIndex = args.length - 1;
2604
+ const handler = args[handlerIndex];
2605
+ if (toolName && typeof handler === "function") {
2606
+ const original = handler;
2607
+ args[handlerIndex] = (...handlerArgs) => {
2608
+ client2.setPendingToolName(toolName);
2609
+ return original(...handlerArgs);
2610
+ };
2611
+ }
2612
+ return value.apply(target, args);
2613
+ };
2614
+ }
2615
+ });
2616
+ }
2617
+
2446
2618
  // ../mcp-core/src/tools/index.ts
2447
2619
  var TOOL_REGISTRARS = {
2448
2620
  naumu_list_graphs: registerListGraphs,
@@ -2480,6 +2652,7 @@ var TOOL_REGISTRARS = {
2480
2652
  naumu_create_thread: registerCreateThread,
2481
2653
  naumu_request_attachment_upload: registerRequestAttachmentUpload,
2482
2654
  naumu_persist_canvas_attachment: registerPersistCanvasAttachment,
2655
+ naumu_get_attachment: registerGetAttachment,
2483
2656
  naumu_add_reaction: registerAddReaction,
2484
2657
  naumu_remove_reaction: registerRemoveReaction,
2485
2658
  naumu_typing: registerNaumuTyping,
@@ -2515,18 +2688,20 @@ var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS).filter(
2515
2688
  (name) => !BOT_ONLY_TOOL_NAMES.has(name)
2516
2689
  );
2517
2690
  function registerAllTools(server2, client2) {
2691
+ const attributed = withToolAttribution(server2, client2);
2518
2692
  for (const [name, registrar] of Object.entries(TOOL_REGISTRARS)) {
2519
2693
  if (BOT_ONLY_TOOL_NAMES.has(name)) continue;
2520
- registrar(server2, client2);
2694
+ registrar(attributed, client2);
2521
2695
  }
2522
2696
  }
2523
2697
  function registerNamedTools(server2, client2, toolNames) {
2698
+ const attributed = withToolAttribution(server2, client2);
2524
2699
  const registered = [];
2525
2700
  const skipped = [];
2526
2701
  for (const name of toolNames) {
2527
2702
  const registrar = TOOL_REGISTRARS[name];
2528
2703
  if (registrar) {
2529
- registrar(server2, client2);
2704
+ registrar(attributed, client2);
2530
2705
  registered.push(name);
2531
2706
  } else {
2532
2707
  skipped.push(name);
@@ -2549,7 +2724,7 @@ if (!apiKey) {
2549
2724
  console.error("Missing required environment variable: NAUMU_API_KEY");
2550
2725
  process.exit(1);
2551
2726
  }
2552
- var client = new NaumuClient(apiUrl, apiKey);
2727
+ var client = new NaumuClient(apiUrl, apiKey, { reportMcpClientHeaders: true });
2553
2728
  var server = new McpServer(
2554
2729
  {
2555
2730
  name: "naumu",
@@ -2578,5 +2753,9 @@ if (isBotKey) {
2578
2753
  `[mcp] connected to ${apiUrl} as user key (${process.env.NAUMU_IDENTITY_ID ?? "no identity id set"})`
2579
2754
  );
2580
2755
  }
2756
+ server.server.oninitialized = () => {
2757
+ const clientInfo = server.server.getClientVersion();
2758
+ client.setMcpClientInfo(clientInfo ? { name: clientInfo.name, version: clientInfo.version } : void 0);
2759
+ };
2581
2760
  var transport = new StdioServerTransport();
2582
2761
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naumu/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "MCP server for Naumu – access your knowledge graph from Claude Code, Cursor, and other AI coding agents",
5
5
  "license": "MIT",
6
6
  "author": "Naumu <hello@naumu.ai>",
@@ -22,7 +22,7 @@
22
22
  "knowledge-management"
23
23
  ],
24
24
  "bin": {
25
- "naumu-mcp": "./dist/index.js"
25
+ "naumu-mcp": "dist/index.js"
26
26
  },
27
27
  "main": "dist/index.js",
28
28
  "type": "module",