@naumu/mcp 0.10.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 +297 -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",
@@ -1131,7 +1211,7 @@ function registerReadThread(server2, client2) {
1131
1211
  {
1132
1212
  title: "Read Thread",
1133
1213
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1134
- 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.',
1135
1215
  inputSchema: z21.object({
1136
1216
  threadId: z21.string().describe("The thread ID to read from."),
1137
1217
  before: z21.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
@@ -1577,8 +1657,80 @@ function registerPersistCanvasAttachment(server2, client2) {
1577
1657
  );
1578
1658
  }
1579
1659
 
1580
- // ../mcp-core/src/tools/add-reaction.ts
1660
+ // ../mcp-core/src/tools/get-attachment.ts
1581
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";
1582
1734
  function registerAddReaction(server2, client2) {
1583
1735
  server2.registerTool(
1584
1736
  "naumu_add_reaction",
@@ -1586,10 +1738,10 @@ function registerAddReaction(server2, client2) {
1586
1738
  title: "Add Reaction",
1587
1739
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1588
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.',
1589
- inputSchema: z30.object({
1590
- threadId: z30.string().describe("Thread containing the message. You must be a participant."),
1591
- messageId: z30.string().describe("The message to react to."),
1592
- 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.')
1593
1745
  })
1594
1746
  },
1595
1747
  async ({ threadId, messageId, emoji }) => {
@@ -1613,7 +1765,7 @@ function registerAddReaction(server2, client2) {
1613
1765
  }
1614
1766
 
1615
1767
  // ../mcp-core/src/tools/remove-reaction.ts
1616
- import { z as z31 } from "zod";
1768
+ import { z as z32 } from "zod";
1617
1769
  function registerRemoveReaction(server2, client2) {
1618
1770
  server2.registerTool(
1619
1771
  "naumu_remove_reaction",
@@ -1621,10 +1773,10 @@ function registerRemoveReaction(server2, client2) {
1621
1773
  title: "Remove Reaction",
1622
1774
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1623
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.",
1624
- inputSchema: z31.object({
1625
- threadId: z31.string().describe("Thread containing the message. You must be a participant."),
1626
- messageId: z31.string().describe("The message to remove your reaction from."),
1627
- 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).")
1628
1780
  })
1629
1781
  },
1630
1782
  async ({ threadId, messageId, emoji }) => {
@@ -1648,7 +1800,7 @@ function registerRemoveReaction(server2, client2) {
1648
1800
  }
1649
1801
 
1650
1802
  // ../mcp-core/src/tools/naumu-typing.ts
1651
- import { z as z32 } from "zod";
1803
+ import { z as z33 } from "zod";
1652
1804
  function registerNaumuTyping(server2, client2) {
1653
1805
  server2.registerTool(
1654
1806
  "naumu_typing",
@@ -1659,9 +1811,9 @@ function registerNaumuTyping(server2, client2) {
1659
1811
  // repeating the same state is a no-op renew, so idempotent.
1660
1812
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1661
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.',
1662
- inputSchema: z32.object({
1663
- threadId: z32.string().describe("The thread ID to set typing in. You must be a participant."),
1664
- 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.')
1665
1817
  })
1666
1818
  },
1667
1819
  async ({ threadId, state }) => {
@@ -1682,7 +1834,7 @@ function registerNaumuTyping(server2, client2) {
1682
1834
  }
1683
1835
 
1684
1836
  // ../mcp-core/src/tools/note-read.ts
1685
- import { z as z33 } from "zod";
1837
+ import { z as z34 } from "zod";
1686
1838
  function registerNoteRead(server2, client2) {
1687
1839
  server2.registerTool(
1688
1840
  "naumu_note_read",
@@ -1690,8 +1842,8 @@ function registerNoteRead(server2, client2) {
1690
1842
  title: "Read Note",
1691
1843
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1692
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.",
1693
- inputSchema: z33.object({
1694
- noteId: z33.string().describe("The note (Thought) ID")
1845
+ inputSchema: z34.object({
1846
+ noteId: z34.string().describe("The note (Thought) ID")
1695
1847
  })
1696
1848
  },
1697
1849
  async ({ noteId }) => {
@@ -1704,7 +1856,7 @@ function registerNoteRead(server2, client2) {
1704
1856
  }
1705
1857
 
1706
1858
  // ../mcp-core/src/tools/note-append.ts
1707
- import { z as z34 } from "zod";
1859
+ import { z as z35 } from "zod";
1708
1860
  function registerNoteAppend(server2, client2) {
1709
1861
  server2.registerTool(
1710
1862
  "naumu_note_append",
@@ -1712,9 +1864,9 @@ function registerNoteAppend(server2, client2) {
1712
1864
  title: "Append to Note",
1713
1865
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1714
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.",
1715
- inputSchema: z34.object({
1716
- noteId: z34.string().describe("The note (Thought) ID to append to"),
1717
- 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")
1718
1870
  })
1719
1871
  },
1720
1872
  async ({ noteId, markdown }) => {
@@ -1727,7 +1879,7 @@ function registerNoteAppend(server2, client2) {
1727
1879
  }
1728
1880
 
1729
1881
  // ../mcp-core/src/tools/note-insert.ts
1730
- import { z as z35 } from "zod";
1882
+ import { z as z36 } from "zod";
1731
1883
  function registerNoteInsert(server2, client2) {
1732
1884
  server2.registerTool(
1733
1885
  "naumu_note_insert",
@@ -1735,10 +1887,10 @@ function registerNoteInsert(server2, client2) {
1735
1887
  title: "Insert After Heading",
1736
1888
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1737
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.",
1738
- inputSchema: z35.object({
1739
- noteId: z35.string().describe("The note (Thought) ID"),
1740
- headingText: z35.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1741
- 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")
1742
1894
  })
1743
1895
  },
1744
1896
  async ({ noteId, headingText, markdown }) => {
@@ -1754,7 +1906,7 @@ function registerNoteInsert(server2, client2) {
1754
1906
  }
1755
1907
 
1756
1908
  // ../mcp-core/src/tools/note-replace-section.ts
1757
- import { z as z36 } from "zod";
1909
+ import { z as z37 } from "zod";
1758
1910
  function registerNoteReplaceSection(server2, client2) {
1759
1911
  server2.registerTool(
1760
1912
  "naumu_note_replace_section",
@@ -1762,11 +1914,11 @@ function registerNoteReplaceSection(server2, client2) {
1762
1914
  title: "Replace Section",
1763
1915
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1764
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.",
1765
- inputSchema: z36.object({
1766
- noteId: z36.string().describe("The note (Thought) ID"),
1767
- headingText: z36.string().min(1).describe("Exact text of the heading anchoring the section"),
1768
- markdown: z36.string().describe("Replacement markdown for the section body"),
1769
- 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.")
1770
1922
  })
1771
1923
  },
1772
1924
  async ({ noteId, headingText, markdown, keepHeading }) => {
@@ -1783,7 +1935,7 @@ function registerNoteReplaceSection(server2, client2) {
1783
1935
  }
1784
1936
 
1785
1937
  // ../mcp-core/src/tools/note-delete-section.ts
1786
- import { z as z37 } from "zod";
1938
+ import { z as z38 } from "zod";
1787
1939
  function registerNoteDeleteSection(server2, client2) {
1788
1940
  server2.registerTool(
1789
1941
  "naumu_note_delete_section",
@@ -1791,9 +1943,9 @@ function registerNoteDeleteSection(server2, client2) {
1791
1943
  title: "Delete Section",
1792
1944
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1793
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.",
1794
- inputSchema: z37.object({
1795
- noteId: z37.string().describe("The note (Thought) ID"),
1796
- 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")
1797
1949
  })
1798
1950
  },
1799
1951
  async ({ noteId, headingText }) => {
@@ -1808,7 +1960,7 @@ function registerNoteDeleteSection(server2, client2) {
1808
1960
  }
1809
1961
 
1810
1962
  // ../mcp-core/src/tools/note-replace.ts
1811
- import { z as z38 } from "zod";
1963
+ import { z as z39 } from "zod";
1812
1964
  function registerNoteReplace(server2, client2) {
1813
1965
  server2.registerTool(
1814
1966
  "naumu_note_replace",
@@ -1816,9 +1968,9 @@ function registerNoteReplace(server2, client2) {
1816
1968
  title: "Replace Note",
1817
1969
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1818
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.",
1819
- inputSchema: z38.object({
1820
- noteId: z38.string().describe("The note (Thought) ID"),
1821
- 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")
1822
1974
  })
1823
1975
  },
1824
1976
  async ({ noteId, markdown }) => {
@@ -1831,7 +1983,7 @@ function registerNoteReplace(server2, client2) {
1831
1983
  }
1832
1984
 
1833
1985
  // ../mcp-core/src/tools/note-find-replace.ts
1834
- import { z as z39 } from "zod";
1986
+ import { z as z40 } from "zod";
1835
1987
  function registerNoteFindReplace(server2, client2) {
1836
1988
  server2.registerTool(
1837
1989
  "naumu_note_find_replace",
@@ -1839,11 +1991,11 @@ function registerNoteFindReplace(server2, client2) {
1839
1991
  title: "Find/Replace in Note",
1840
1992
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1841
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.",
1842
- inputSchema: z39.object({
1843
- noteId: z39.string().describe("The note (Thought) ID"),
1844
- find: z39.string().min(1).describe("Substring to search for. Literal - no regex."),
1845
- replace: z39.string().describe("Replacement string. May be empty to delete the match."),
1846
- 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.")
1847
1999
  })
1848
2000
  },
1849
2001
  async ({ noteId, find, replace, all }) => {
@@ -1860,23 +2012,24 @@ function registerNoteFindReplace(server2, client2) {
1860
2012
  }
1861
2013
 
1862
2014
  // ../mcp-core/src/tools/create-note.ts
1863
- import { z as z40 } from "zod";
2015
+ import { z as z41 } from "zod";
1864
2016
  function registerCreateNote(server2, client2) {
1865
2017
  server2.registerTool(
1866
2018
  "naumu_create_note",
1867
2019
  {
1868
2020
  title: "Create Note",
1869
2021
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1870
- description: "Create a new empty note in a graph; use when you need a fresh note to write into. Returns the new note row including its `id` - pass that id to `naumu_note_append` / `naumu_note_replace` to fill in the content. 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 \u2014 a bot passing `topicIds` is rejected (bots hold no topic membership); use `sharedWithSpace` instead.",
1871
- inputSchema: z40.object({
1872
- graphId: z40.string().describe("The graph ID to create the note in"),
1873
- title: z40.string().optional().describe("Optional title for the note"),
1874
- sharedWithSpace: z40.boolean().optional().describe("Share the note with everyone in the space. Omit (or false) to keep it private to its participants."),
1875
- 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.")
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.",
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.")
1876
2029
  })
1877
2030
  },
1878
- async ({ graphId, title, sharedWithSpace, topicIds }) => {
1879
- const data = await client2.post("/api/notes", { graphId, title, sharedWithSpace, topicIds });
2031
+ async ({ graphId, title, markdown, sharedWithSpace, topicIds }) => {
2032
+ const data = await client2.post("/api/notes", { graphId, title, markdown, sharedWithSpace, topicIds });
1880
2033
  return {
1881
2034
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1882
2035
  };
@@ -1885,7 +2038,7 @@ function registerCreateNote(server2, client2) {
1885
2038
  }
1886
2039
 
1887
2040
  // ../mcp-core/src/tools/list-schema-violations.ts
1888
- import { z as z41 } from "zod";
2041
+ import { z as z42 } from "zod";
1889
2042
  var DEFAULT_EXAMPLE_LIMIT = 5;
1890
2043
  var rowsForKind = (violations, kind) => {
1891
2044
  const rows = [];
@@ -1911,12 +2064,12 @@ function registerListSchemaViolations(server2, client2) {
1911
2064
  title: "List Schema Violations",
1912
2065
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1913
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.",
1914
- inputSchema: z41.object({
1915
- graphId: z41.string().describe("The graph ID"),
1916
- kind: z41.string().optional().describe(
2067
+ inputSchema: z42.object({
2068
+ graphId: z42.string().describe("The graph ID"),
2069
+ kind: z42.string().optional().describe(
1917
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.'
1918
2071
  ),
1919
- limit: z41.number().int().min(1).optional().describe(
2072
+ limit: z42.number().int().min(1).optional().describe(
1920
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)."
1921
2074
  )
1922
2075
  })
@@ -1968,7 +2121,7 @@ function registerListSchemaViolations(server2, client2) {
1968
2121
  }
1969
2122
 
1970
2123
  // ../mcp-core/src/tools/list-dense-nodes.ts
1971
- import { z as z42 } from "zod";
2124
+ import { z as z43 } from "zod";
1972
2125
  function registerListDenseNodes(server2, client2) {
1973
2126
  server2.registerTool(
1974
2127
  "naumu_list_dense_nodes",
@@ -1976,10 +2129,10 @@ function registerListDenseNodes(server2, client2) {
1976
2129
  title: "List Dense Nodes",
1977
2130
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1978
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.',
1979
- inputSchema: z42.object({
1980
- graphId: z42.string().describe("The graph ID"),
1981
- 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."),
1982
- 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.")
1983
2136
  })
1984
2137
  },
1985
2138
  async ({ graphId, minConnections, nodeTypes }) => {
@@ -1997,7 +2150,7 @@ function registerListDenseNodes(server2, client2) {
1997
2150
  }
1998
2151
 
1999
2152
  // ../mcp-core/src/tools/list-node-connections.ts
2000
- import { z as z43 } from "zod";
2153
+ import { z as z44 } from "zod";
2001
2154
  function registerListNodeConnections(server2, client2) {
2002
2155
  server2.registerTool(
2003
2156
  "naumu_list_node_connections",
@@ -2005,11 +2158,11 @@ function registerListNodeConnections(server2, client2) {
2005
2158
  title: "List Node Connections",
2006
2159
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2007
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}}] }`.',
2008
- inputSchema: z43.object({
2009
- graphId: z43.string().describe("The graph ID"),
2010
- nodeId: z43.string().describe("The node ID to inspect"),
2011
- edgeType: z43.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
2012
- 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).')
2013
2166
  })
2014
2167
  },
2015
2168
  async ({ graphId, nodeId, edgeType, direction }) => {
@@ -2027,7 +2180,7 @@ function registerListNodeConnections(server2, client2) {
2027
2180
  }
2028
2181
 
2029
2182
  // ../mcp-core/src/tools/reparent.ts
2030
- import { z as z44 } from "zod";
2183
+ import { z as z45 } from "zod";
2031
2184
  function registerReparent(server2, client2) {
2032
2185
  server2.registerTool(
2033
2186
  "naumu_reparent",
@@ -2035,11 +2188,11 @@ function registerReparent(server2, client2) {
2035
2188
  title: "Reparent Node",
2036
2189
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2037
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"}`.',
2038
- inputSchema: z44.object({
2039
- graphId: z44.string().describe("The graph ID"),
2040
- nodeId: z44.string().describe("The child node to reparent"),
2041
- newParentId: z44.string().describe("The new parent node id"),
2042
- 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).')
2043
2196
  })
2044
2197
  },
2045
2198
  async ({ graphId, nodeId, newParentId, newRelation }) => {
@@ -2055,7 +2208,7 @@ function registerReparent(server2, client2) {
2055
2208
  }
2056
2209
 
2057
2210
  // ../mcp-core/src/tools/batch-reparent.ts
2058
- import { z as z45 } from "zod";
2211
+ import { z as z46 } from "zod";
2059
2212
  function registerBatchReparent(server2, client2) {
2060
2213
  server2.registerTool(
2061
2214
  "naumu_batch_reparent",
@@ -2063,11 +2216,11 @@ function registerBatchReparent(server2, client2) {
2063
2216
  title: "Batch Reparent Nodes",
2064
2217
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2065
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?}]`.',
2066
- inputSchema: z45.object({
2067
- graphId: z45.string().describe("The graph ID"),
2068
- newParentId: z45.string().describe("Parent node id every nodeId in the batch will be parented to"),
2069
- newRelation: z45.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
2070
- 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`")
2071
2224
  })
2072
2225
  },
2073
2226
  async ({ graphId, newParentId, newRelation, nodeIds }) => {
@@ -2084,7 +2237,7 @@ function registerBatchReparent(server2, client2) {
2084
2237
  }
2085
2238
 
2086
2239
  // ../mcp-core/src/tools/chatgpt-search.ts
2087
- import { z as z46 } from "zod";
2240
+ import { z as z47 } from "zod";
2088
2241
 
2089
2242
  // ../mcp-core/src/public-origin.ts
2090
2243
  function publicOrigin() {
@@ -2138,8 +2291,8 @@ function registerChatgptSearch(server2, client2) {
2138
2291
  title: "Search",
2139
2292
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2140
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.)",
2141
- inputSchema: z46.object({
2142
- query: z46.string().describe(
2294
+ inputSchema: z47.object({
2295
+ query: z47.string().describe(
2143
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."
2144
2297
  )
2145
2298
  })
@@ -2173,7 +2326,7 @@ function registerChatgptSearch(server2, client2) {
2173
2326
  }
2174
2327
 
2175
2328
  // ../mcp-core/src/tools/chatgpt-fetch.ts
2176
- import { z as z47 } from "zod";
2329
+ import { z as z48 } from "zod";
2177
2330
  var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2178
2331
  "id",
2179
2332
  "label",
@@ -2238,8 +2391,8 @@ function registerChatgptFetch(server2, client2) {
2238
2391
  title: "Fetch",
2239
2392
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2240
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.",
2241
- inputSchema: z47.object({
2242
- 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>`.")
2243
2396
  })
2244
2397
  },
2245
2398
  async ({ id }) => {
@@ -2282,7 +2435,7 @@ function registerChatgptFetch(server2, client2) {
2282
2435
  }
2283
2436
 
2284
2437
  // ../mcp-core/src/tools/admission-status.ts
2285
- import { z as z48 } from "zod";
2438
+ import { z as z49 } from "zod";
2286
2439
  function registerAdmissionStatus(server2, client2) {
2287
2440
  server2.registerTool(
2288
2441
  "naumu_admission_status",
@@ -2290,8 +2443,8 @@ function registerAdmissionStatus(server2, client2) {
2290
2443
  title: "Admission Status",
2291
2444
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
2292
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.",
2293
- inputSchema: z48.object({
2294
- 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.")
2295
2448
  })
2296
2449
  },
2297
2450
  async ({ graphId }) => {
@@ -2321,7 +2474,7 @@ function registerAdmissionStatus(server2, client2) {
2321
2474
  }
2322
2475
 
2323
2476
  // ../mcp-core/src/tools/whitelist-members.ts
2324
- import { z as z49 } from "zod";
2477
+ import { z as z50 } from "zod";
2325
2478
  function registerWhitelistMembers(server2, client2) {
2326
2479
  server2.registerTool(
2327
2480
  "naumu_whitelist_members",
@@ -2334,10 +2487,10 @@ function registerWhitelistMembers(server2, client2) {
2334
2487
  openWorldHint: false
2335
2488
  },
2336
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.",
2337
- inputSchema: z49.object({
2338
- graphId: z49.string().describe("The space (graph) ID to whitelist emails for. You must be a member of this space."),
2339
- 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."),
2340
- 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.")
2341
2494
  })
2342
2495
  },
2343
2496
  async ({ graphId, emails, repoInit }) => {
@@ -2361,7 +2514,7 @@ function registerWhitelistMembers(server2, client2) {
2361
2514
  }
2362
2515
 
2363
2516
  // ../mcp-core/src/tools/resolve-admission.ts
2364
- import { z as z50 } from "zod";
2517
+ import { z as z51 } from "zod";
2365
2518
  function registerResolveAdmission(server2, client2) {
2366
2519
  server2.registerTool(
2367
2520
  "naumu_resolve_admission",
@@ -2374,9 +2527,9 @@ function registerResolveAdmission(server2, client2) {
2374
2527
  openWorldHint: false
2375
2528
  },
2376
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.",
2377
- inputSchema: z50.object({
2378
- graphId: z50.string().describe("The space (graph) ID referenced by the repo .naumu file that the user wants to join."),
2379
- 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.")
2380
2533
  })
2381
2534
  },
2382
2535
  async ({ graphId, gitEmailHint }) => {
@@ -2400,7 +2553,7 @@ function registerResolveAdmission(server2, client2) {
2400
2553
  }
2401
2554
 
2402
2555
  // ../mcp-core/src/tools/resolve-join-request.ts
2403
- import { z as z51 } from "zod";
2556
+ import { z as z52 } from "zod";
2404
2557
  function registerResolveJoinRequest(server2, client2) {
2405
2558
  server2.registerTool(
2406
2559
  "naumu_resolve_join_request",
@@ -2413,10 +2566,10 @@ function registerResolveJoinRequest(server2, client2) {
2413
2566
  openWorldHint: false
2414
2567
  },
2415
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.",
2416
- inputSchema: z51.object({
2417
- graphId: z51.string().describe("The space (graph) ID the request is for. You must be a member of this space."),
2418
- requestId: z51.string().describe("The pending join request ID, taken from naumu_admission_status."),
2419
- 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.")
2420
2573
  })
2421
2574
  },
2422
2575
  async ({ graphId, requestId, action }) => {
@@ -2439,6 +2592,29 @@ function registerResolveJoinRequest(server2, client2) {
2439
2592
  );
2440
2593
  }
2441
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
+
2442
2618
  // ../mcp-core/src/tools/index.ts
2443
2619
  var TOOL_REGISTRARS = {
2444
2620
  naumu_list_graphs: registerListGraphs,
@@ -2476,6 +2652,7 @@ var TOOL_REGISTRARS = {
2476
2652
  naumu_create_thread: registerCreateThread,
2477
2653
  naumu_request_attachment_upload: registerRequestAttachmentUpload,
2478
2654
  naumu_persist_canvas_attachment: registerPersistCanvasAttachment,
2655
+ naumu_get_attachment: registerGetAttachment,
2479
2656
  naumu_add_reaction: registerAddReaction,
2480
2657
  naumu_remove_reaction: registerRemoveReaction,
2481
2658
  naumu_typing: registerNaumuTyping,
@@ -2511,18 +2688,20 @@ var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS).filter(
2511
2688
  (name) => !BOT_ONLY_TOOL_NAMES.has(name)
2512
2689
  );
2513
2690
  function registerAllTools(server2, client2) {
2691
+ const attributed = withToolAttribution(server2, client2);
2514
2692
  for (const [name, registrar] of Object.entries(TOOL_REGISTRARS)) {
2515
2693
  if (BOT_ONLY_TOOL_NAMES.has(name)) continue;
2516
- registrar(server2, client2);
2694
+ registrar(attributed, client2);
2517
2695
  }
2518
2696
  }
2519
2697
  function registerNamedTools(server2, client2, toolNames) {
2698
+ const attributed = withToolAttribution(server2, client2);
2520
2699
  const registered = [];
2521
2700
  const skipped = [];
2522
2701
  for (const name of toolNames) {
2523
2702
  const registrar = TOOL_REGISTRARS[name];
2524
2703
  if (registrar) {
2525
- registrar(server2, client2);
2704
+ registrar(attributed, client2);
2526
2705
  registered.push(name);
2527
2706
  } else {
2528
2707
  skipped.push(name);
@@ -2545,11 +2724,11 @@ if (!apiKey) {
2545
2724
  console.error("Missing required environment variable: NAUMU_API_KEY");
2546
2725
  process.exit(1);
2547
2726
  }
2548
- var client = new NaumuClient(apiUrl, apiKey);
2727
+ var client = new NaumuClient(apiUrl, apiKey, { reportMcpClientHeaders: true });
2549
2728
  var server = new McpServer(
2550
2729
  {
2551
2730
  name: "naumu",
2552
- version: "0.4.0"
2731
+ version: NAUMU_MCP_VERSION
2553
2732
  },
2554
2733
  {
2555
2734
  instructions: NAUMU_INSTRUCTIONS
@@ -2574,5 +2753,9 @@ if (isBotKey) {
2574
2753
  `[mcp] connected to ${apiUrl} as user key (${process.env.NAUMU_IDENTITY_ID ?? "no identity id set"})`
2575
2754
  );
2576
2755
  }
2756
+ server.server.oninitialized = () => {
2757
+ const clientInfo = server.server.getClientVersion();
2758
+ client.setMcpClientInfo(clientInfo ? { name: clientInfo.name, version: clientInfo.version } : void 0);
2759
+ };
2577
2760
  var transport = new StdioServerTransport();
2578
2761
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naumu/mcp",
3
- "version": "0.10.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",