@naumu/mcp 0.6.0 → 0.6.2

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 (178) hide show
  1. package/dist/index.js +2314 -49
  2. package/package.json +6 -4
  3. package/dist/client.d.ts +0 -15
  4. package/dist/client.js +0 -82
  5. package/dist/client.js.map +0 -1
  6. package/dist/errors.d.ts +0 -35
  7. package/dist/errors.js +0 -52
  8. package/dist/errors.js.map +0 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.js.map +0 -1
  11. package/dist/tools/add-attribute.d.ts +0 -3
  12. package/dist/tools/add-attribute.js +0 -58
  13. package/dist/tools/add-attribute.js.map +0 -1
  14. package/dist/tools/add-connection.d.ts +0 -3
  15. package/dist/tools/add-connection.js +0 -46
  16. package/dist/tools/add-connection.js.map +0 -1
  17. package/dist/tools/add-edge.d.ts +0 -3
  18. package/dist/tools/add-edge.js +0 -33
  19. package/dist/tools/add-edge.js.map +0 -1
  20. package/dist/tools/add-node-type.d.ts +0 -3
  21. package/dist/tools/add-node-type.js +0 -67
  22. package/dist/tools/add-node-type.js.map +0 -1
  23. package/dist/tools/add-node.d.ts +0 -3
  24. package/dist/tools/add-node.js +0 -36
  25. package/dist/tools/add-node.js.map +0 -1
  26. package/dist/tools/add-reaction.d.ts +0 -3
  27. package/dist/tools/add-reaction.js +0 -34
  28. package/dist/tools/add-reaction.js.map +0 -1
  29. package/dist/tools/ask-naumu.d.ts +0 -3
  30. package/dist/tools/ask-naumu.js +0 -44
  31. package/dist/tools/ask-naumu.js.map +0 -1
  32. package/dist/tools/ask.d.ts +0 -3
  33. package/dist/tools/ask.js +0 -37
  34. package/dist/tools/ask.js.map +0 -1
  35. package/dist/tools/batch-reparent.d.ts +0 -3
  36. package/dist/tools/batch-reparent.js +0 -31
  37. package/dist/tools/batch-reparent.js.map +0 -1
  38. package/dist/tools/canvas-add-element.d.ts +0 -3
  39. package/dist/tools/canvas-add-element.js +0 -46
  40. package/dist/tools/canvas-add-element.js.map +0 -1
  41. package/dist/tools/canvas-remove-element.d.ts +0 -3
  42. package/dist/tools/canvas-remove-element.js +0 -17
  43. package/dist/tools/canvas-remove-element.js.map +0 -1
  44. package/dist/tools/canvas-update-element.d.ts +0 -3
  45. package/dist/tools/canvas-update-element.js +0 -20
  46. package/dist/tools/canvas-update-element.js.map +0 -1
  47. package/dist/tools/create-canvas.d.ts +0 -3
  48. package/dist/tools/create-canvas.js +0 -17
  49. package/dist/tools/create-canvas.js.map +0 -1
  50. package/dist/tools/create-graph.d.ts +0 -3
  51. package/dist/tools/create-graph.js +0 -19
  52. package/dist/tools/create-graph.js.map +0 -1
  53. package/dist/tools/create-note.d.ts +0 -3
  54. package/dist/tools/create-note.js +0 -17
  55. package/dist/tools/create-note.js.map +0 -1
  56. package/dist/tools/create-thread.d.ts +0 -3
  57. package/dist/tools/create-thread.js +0 -63
  58. package/dist/tools/create-thread.js.map +0 -1
  59. package/dist/tools/filter.d.ts +0 -3
  60. package/dist/tools/filter.js +0 -54
  61. package/dist/tools/filter.js.map +0 -1
  62. package/dist/tools/get-ai-thread.d.ts +0 -3
  63. package/dist/tools/get-ai-thread.js +0 -18
  64. package/dist/tools/get-ai-thread.js.map +0 -1
  65. package/dist/tools/get-canvas-elements.d.ts +0 -3
  66. package/dist/tools/get-canvas-elements.js +0 -16
  67. package/dist/tools/get-canvas-elements.js.map +0 -1
  68. package/dist/tools/get-canvas-image.d.ts +0 -3
  69. package/dist/tools/get-canvas-image.js +0 -77
  70. package/dist/tools/get-canvas-image.js.map +0 -1
  71. package/dist/tools/get-node.d.ts +0 -3
  72. package/dist/tools/get-node.js +0 -17
  73. package/dist/tools/get-node.js.map +0 -1
  74. package/dist/tools/get-schema.d.ts +0 -3
  75. package/dist/tools/get-schema.js +0 -55
  76. package/dist/tools/get-schema.js.map +0 -1
  77. package/dist/tools/get-thread.d.ts +0 -3
  78. package/dist/tools/get-thread.js +0 -25
  79. package/dist/tools/get-thread.js.map +0 -1
  80. package/dist/tools/get-view.d.ts +0 -3
  81. package/dist/tools/get-view.js +0 -17
  82. package/dist/tools/get-view.js.map +0 -1
  83. package/dist/tools/index.d.ts +0 -13
  84. package/dist/tools/index.js +0 -147
  85. package/dist/tools/index.js.map +0 -1
  86. package/dist/tools/list-canvases.d.ts +0 -3
  87. package/dist/tools/list-canvases.js +0 -16
  88. package/dist/tools/list-canvases.js.map +0 -1
  89. package/dist/tools/list-dense-nodes.d.ts +0 -3
  90. package/dist/tools/list-dense-nodes.js +0 -29
  91. package/dist/tools/list-dense-nodes.js.map +0 -1
  92. package/dist/tools/list-graphs.d.ts +0 -3
  93. package/dist/tools/list-graphs.js +0 -14
  94. package/dist/tools/list-graphs.js.map +0 -1
  95. package/dist/tools/list-node-connections.d.ts +0 -3
  96. package/dist/tools/list-node-connections.js +0 -32
  97. package/dist/tools/list-node-connections.js.map +0 -1
  98. package/dist/tools/list-schema-violations.d.ts +0 -3
  99. package/dist/tools/list-schema-violations.js +0 -16
  100. package/dist/tools/list-schema-violations.js.map +0 -1
  101. package/dist/tools/list-threads.d.ts +0 -3
  102. package/dist/tools/list-threads.js +0 -43
  103. package/dist/tools/list-threads.js.map +0 -1
  104. package/dist/tools/list-view-nodes.d.ts +0 -3
  105. package/dist/tools/list-view-nodes.js +0 -29
  106. package/dist/tools/list-view-nodes.js.map +0 -1
  107. package/dist/tools/naumu-typing.d.ts +0 -3
  108. package/dist/tools/naumu-typing.js +0 -30
  109. package/dist/tools/naumu-typing.js.map +0 -1
  110. package/dist/tools/note-append.d.ts +0 -3
  111. package/dist/tools/note-append.js +0 -17
  112. package/dist/tools/note-append.js.map +0 -1
  113. package/dist/tools/note-delete-section.d.ts +0 -3
  114. package/dist/tools/note-delete-section.js +0 -22
  115. package/dist/tools/note-delete-section.js.map +0 -1
  116. package/dist/tools/note-find-replace.d.ts +0 -3
  117. package/dist/tools/note-find-replace.js +0 -26
  118. package/dist/tools/note-find-replace.js.map +0 -1
  119. package/dist/tools/note-insert.d.ts +0 -3
  120. package/dist/tools/note-insert.js +0 -24
  121. package/dist/tools/note-insert.js.map +0 -1
  122. package/dist/tools/note-read.d.ts +0 -3
  123. package/dist/tools/note-read.js +0 -16
  124. package/dist/tools/note-read.js.map +0 -1
  125. package/dist/tools/note-replace-section.d.ts +0 -3
  126. package/dist/tools/note-replace-section.js +0 -26
  127. package/dist/tools/note-replace-section.js.map +0 -1
  128. package/dist/tools/note-replace.d.ts +0 -3
  129. package/dist/tools/note-replace.js +0 -17
  130. package/dist/tools/note-replace.js.map +0 -1
  131. package/dist/tools/post-message.d.ts +0 -3
  132. package/dist/tools/post-message.js +0 -45
  133. package/dist/tools/post-message.js.map +0 -1
  134. package/dist/tools/react-to-message.d.ts +0 -3
  135. package/dist/tools/react-to-message.js +0 -34
  136. package/dist/tools/react-to-message.js.map +0 -1
  137. package/dist/tools/read-thread.d.ts +0 -3
  138. package/dist/tools/read-thread.js +0 -43
  139. package/dist/tools/read-thread.js.map +0 -1
  140. package/dist/tools/remove-edge.d.ts +0 -3
  141. package/dist/tools/remove-edge.js +0 -25
  142. package/dist/tools/remove-edge.js.map +0 -1
  143. package/dist/tools/remove-edges-bulk.d.ts +0 -3
  144. package/dist/tools/remove-edges-bulk.js +0 -26
  145. package/dist/tools/remove-edges-bulk.js.map +0 -1
  146. package/dist/tools/remove-node.d.ts +0 -3
  147. package/dist/tools/remove-node.js +0 -17
  148. package/dist/tools/remove-node.js.map +0 -1
  149. package/dist/tools/remove-reaction.d.ts +0 -3
  150. package/dist/tools/remove-reaction.js +0 -34
  151. package/dist/tools/remove-reaction.js.map +0 -1
  152. package/dist/tools/reparent.d.ts +0 -3
  153. package/dist/tools/reparent.js +0 -21
  154. package/dist/tools/reparent.js.map +0 -1
  155. package/dist/tools/request-attachment-upload.d.ts +0 -3
  156. package/dist/tools/request-attachment-upload.js +0 -59
  157. package/dist/tools/request-attachment-upload.js.map +0 -1
  158. package/dist/tools/search.d.ts +0 -3
  159. package/dist/tools/search.js +0 -34
  160. package/dist/tools/search.js.map +0 -1
  161. package/dist/tools/semantic-search.d.ts +0 -3
  162. package/dist/tools/semantic-search.js +0 -34
  163. package/dist/tools/semantic-search.js.map +0 -1
  164. package/dist/tools/text-search.d.ts +0 -3
  165. package/dist/tools/text-search.js +0 -18
  166. package/dist/tools/text-search.js.map +0 -1
  167. package/dist/tools/traverse.d.ts +0 -3
  168. package/dist/tools/traverse.js +0 -19
  169. package/dist/tools/traverse.js.map +0 -1
  170. package/dist/tools/update-node.d.ts +0 -30
  171. package/dist/tools/update-node.js +0 -152
  172. package/dist/tools/update-node.js.map +0 -1
  173. package/dist/tools/update-schema.d.ts +0 -3
  174. package/dist/tools/update-schema.js +0 -60
  175. package/dist/tools/update-schema.js.map +0 -1
  176. package/dist/tools/whoami.d.ts +0 -3
  177. package/dist/tools/whoami.js +0 -23
  178. package/dist/tools/whoami.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,68 +1,2333 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { NaumuClient } from './client.js';
5
- import { registerAllTools, registerNamedTools } from './tools/index.js';
6
- const DEFAULT_API_URL = 'https://naumu.ai';
7
- const BOT_KEY_PREFIX = 'nmu_bot_';
8
- const apiUrl = (process.env.NAUMU_API_URL || DEFAULT_API_URL).replace(/\/api\/?$/, '');
9
- const apiKey = process.env.NAUMU_API_KEY;
10
- if (!apiKey) {
11
- console.error('Missing required environment variable: NAUMU_API_KEY');
12
- process.exit(1);
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // ../mcp-core/src/errors.ts
8
+ var NaumuApiError = class extends Error {
9
+ constructor(status, message, upstreamMessage) {
10
+ super(message);
11
+ this.status = status;
12
+ this.upstreamMessage = upstreamMessage;
13
+ this.name = "NaumuApiError";
14
+ }
15
+ status;
16
+ upstreamMessage;
17
+ };
18
+ function safeErrorMessage(status, upstream) {
19
+ if (status === 400 || status === 422) return upstream;
20
+ if (status === 401) return "Unauthorized \u2014 check your API key.";
21
+ if (status === 403 || status === 404) {
22
+ return "Resource not found, or you do not have access to it.";
23
+ }
24
+ if (status === 409) return "Conflict with current state.";
25
+ if (status === 410) return "Resource is no longer available.";
26
+ if (status === 413) return "Payload too large.";
27
+ if (status === 429) return "Rate limit exceeded \u2014 slow down and retry.";
28
+ if (status >= 500) return "Server error \u2014 try again shortly.";
29
+ return `Request failed with status ${status}.`;
13
30
  }
14
- const NAUMU_INSTRUCTIONS = `The Naumu MCP server gives structured access to Naumu knowledge graphs (also called spaces). Prefer these tools over WebFetch whenever the user mentions a naumu.ai URL — Naumu pages are client-rendered React, so WebFetch returns an empty shell with no data.
31
+
32
+ // ../mcp-core/src/client.ts
33
+ var NaumuClient = class {
34
+ baseUrl;
35
+ apiKey;
36
+ constructor(baseUrl, apiKey2) {
37
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
38
+ this.apiKey = apiKey2;
39
+ }
40
+ headers() {
41
+ return {
42
+ "Authorization": `Bearer ${this.apiKey}`,
43
+ "Content-Type": "application/json"
44
+ };
45
+ }
46
+ async handleResponse(res) {
47
+ if (!res.ok) {
48
+ const text = await res.text().catch(() => "Unknown error");
49
+ let upstream;
50
+ try {
51
+ const json = JSON.parse(text);
52
+ upstream = json.message || text;
53
+ } catch {
54
+ upstream = text;
55
+ }
56
+ console.error(
57
+ `[mcp] upstream ${res.status}: ${upstream.slice(0, 500)}`
58
+ );
59
+ throw new NaumuApiError(res.status, safeErrorMessage(res.status, upstream), upstream);
60
+ }
61
+ return res.json();
62
+ }
63
+ async get(path) {
64
+ const res = await fetch(`${this.baseUrl}${path}`, {
65
+ method: "GET",
66
+ headers: this.headers()
67
+ });
68
+ return this.handleResponse(res);
69
+ }
70
+ async getBinary(path) {
71
+ const res = await fetch(`${this.baseUrl}${path}`, {
72
+ method: "GET",
73
+ headers: { Authorization: `Bearer ${this.apiKey}` }
74
+ });
75
+ if (!res.ok) {
76
+ const upstream = await res.text().catch(() => "Unknown error");
77
+ console.error(
78
+ `[mcp] upstream binary ${res.status}: ${upstream.slice(0, 500)}`
79
+ );
80
+ throw new NaumuApiError(
81
+ res.status,
82
+ safeErrorMessage(res.status, upstream),
83
+ upstream
84
+ );
85
+ }
86
+ const arrayBuffer = await res.arrayBuffer();
87
+ return {
88
+ buffer: new Uint8Array(arrayBuffer),
89
+ contentType: res.headers.get("content-type") ?? "application/octet-stream"
90
+ };
91
+ }
92
+ async post(path, body) {
93
+ const res = await fetch(`${this.baseUrl}${path}`, {
94
+ method: "POST",
95
+ headers: this.headers(),
96
+ body: body !== void 0 ? JSON.stringify(body) : void 0
97
+ });
98
+ return this.handleResponse(res);
99
+ }
100
+ async patch(path, body) {
101
+ const res = await fetch(`${this.baseUrl}${path}`, {
102
+ method: "PATCH",
103
+ headers: this.headers(),
104
+ body: JSON.stringify(body)
105
+ });
106
+ return this.handleResponse(res);
107
+ }
108
+ async del(path, body) {
109
+ const res = await fetch(`${this.baseUrl}${path}`, {
110
+ method: "DELETE",
111
+ headers: this.headers(),
112
+ body: body !== void 0 ? JSON.stringify(body) : void 0
113
+ });
114
+ return this.handleResponse(res);
115
+ }
116
+ };
117
+
118
+ // ../mcp-core/src/instructions.ts
119
+ var NAUMU_INSTRUCTIONS = `The Naumu MCP server gives structured access to Naumu knowledge graphs (also called spaces). Prefer these tools over WebFetch whenever the user mentions a naumu.ai URL \u2014 Naumu pages are client-rendered React, so WebFetch returns an empty shell with no data.
15
120
 
16
121
  IMPORTANT: graphId is a UUID (e.g. "0464cbfa-60ca-41b3-ac8f-bbeb8243a193"). The value in the URL right after /spaces/ is a slug (e.g. "naumu-0464cbfa"), NOT the graphId. You must resolve the slug to a graphId first.
17
122
 
18
- How to resolve a slug graphId:
123
+ How to resolve a slug \u2192 graphId:
19
124
  Call naumu_list_graphs and find the graph whose 'slug' field matches the URL segment. Use that graph's 'id' as graphId for every other tool. Cache it for the rest of the conversation; the user will likely paste more URLs for the same space.
20
125
 
21
- URL tool mapping (the value after /spaces/ is the slug resolve it first):
22
- - naumu.ai/spaces/{slug} naumu_list_graphs (resolve), then naumu_get_schema for an overview
23
- - naumu.ai/spaces/{slug}/views/{viewId} naumu_list_graphs (resolve), then naumu_get_view, then naumu_list_view_nodes
24
- - naumu.ai/spaces/{slug}/nodes/{nodeId} naumu_list_graphs (resolve), then naumu_get_node
25
- - naumu.ai/spaces/{slug}/chat/{threadId} naumu_list_graphs (resolve), then naumu_get_ai_thread
26
- - Other panel URLs (notes, canvases, conversations, members, settings, schema, heat, health, changelog) have no dedicated tool fall back to naumu_get_node with the relevant id, or naumu_get_schema for the space-level question.
126
+ URL \u2192 tool mapping (the value after /spaces/ is the slug \u2014 resolve it first):
127
+ - naumu.ai/spaces/{slug} \u2192 naumu_list_graphs (resolve), then naumu_get_schema for an overview
128
+ - naumu.ai/spaces/{slug}/views/{viewId} \u2192 naumu_list_graphs (resolve), then naumu_get_view, then naumu_list_view_nodes
129
+ - naumu.ai/spaces/{slug}/nodes/{nodeId} \u2192 naumu_list_graphs (resolve), then naumu_get_node
130
+ - naumu.ai/spaces/{slug}/chat/{threadId} \u2192 naumu_list_graphs (resolve), then naumu_get_ai_thread
131
+ - Other panel URLs (notes, canvases, conversations, members, settings, schema, heat, health, changelog) have no dedicated tool \u2014 fall back to naumu_get_node with the relevant id, or naumu_get_schema for the space-level question.
27
132
 
28
133
  Recommended workflow when a user pastes a view URL:
29
- 1. naumu_list_graphs find the graph whose slug matches the URL. Note its 'id' (the UUID) as graphId.
30
- 2. naumu_get_view {graphId, viewId} read the returned 'summary' and 'filters' to understand what the view returns. This is cheap.
31
- 3. naumu_list_view_nodes {graphId, viewId} page through results.
32
- - fields:"summary" (default) for {id, label, type} per node best for browsing.
33
- - fields:"id" for {id} only best when you just need to count or iterate.
34
- - fields:"full" for the complete node payload only when you need every attribute.
134
+ 1. naumu_list_graphs \u2014 find the graph whose slug matches the URL. Note its 'id' (the UUID) as graphId.
135
+ 2. naumu_get_view {graphId, viewId} \u2014 read the returned 'summary' and 'filters' to understand what the view returns. This is cheap.
136
+ 3. naumu_list_view_nodes {graphId, viewId} \u2014 page through results.
137
+ - fields:"summary" (default) for {id, label, type} per node \u2014 best for browsing.
138
+ - fields:"id" for {id} only \u2014 best when you just need to count or iterate.
139
+ - fields:"full" for the complete node payload \u2014 only when you need every attribute.
35
140
  - The response always includes totalCount, so you don't need to page through everything to get a count.
36
141
  4. naumu_get_node {graphId, nodeId} for per-node detail when needed.
37
142
 
38
- Localhost URLs (http://localhost:3000/spaces/{slug}/...) follow the same shape resolve the slug the same way. The MCP backend host is configured separately via NAUMU_API_URL; the URL the user pastes is just for parsing structure.`;
39
- const client = new NaumuClient(apiUrl, apiKey);
40
- const server = new McpServer({
41
- name: 'naumu',
42
- version: '0.4.0',
43
- }, {
44
- instructions: NAUMU_INSTRUCTIONS,
143
+ 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.`;
144
+
145
+ // ../mcp-core/src/tools/list-graphs.ts
146
+ import { z } from "zod";
147
+ function registerListGraphs(server2, client2) {
148
+ server2.registerTool(
149
+ "naumu_list_graphs",
150
+ {
151
+ title: "List Graphs",
152
+ annotations: { readOnlyHint: true },
153
+ description: "List all knowledge graphs (spaces) the authenticated user has access to. Returns graph IDs, names, and roles.",
154
+ inputSchema: z.object({})
155
+ },
156
+ async () => {
157
+ const data = await client2.get("/api/graphs");
158
+ return {
159
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
160
+ };
161
+ }
162
+ );
163
+ }
164
+
165
+ // ../mcp-core/src/tools/create-graph.ts
166
+ import { z as z2 } from "zod";
167
+ function registerCreateGraph(server2, client2) {
168
+ server2.registerTool(
169
+ "naumu_create_graph",
170
+ {
171
+ title: "Create Graph",
172
+ annotations: { destructiveHint: true },
173
+ description: "Create a new knowledge graph (space) owned by the authenticated user. Returns `{id, name, slug, role, memberRole, createdAt, onboardingThreadId}` \u2014 use the returned `id` as `graphId` for subsequent tool calls. The space starts empty (no schema, no nodes); follow up with `naumu_update_schema` to register types before any `naumu_add_node` calls.",
174
+ inputSchema: z2.object({
175
+ name: z2.string().min(1).describe("Display name for the new space. A URL slug is generated from this name.")
176
+ })
177
+ },
178
+ async ({ name }) => {
179
+ const data = await client2.post("/api/graphs", { name });
180
+ return {
181
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
182
+ };
183
+ }
184
+ );
185
+ }
186
+
187
+ // ../mcp-core/src/tools/get-schema.ts
188
+ import { z as z3 } from "zod";
189
+ function formatSchema(schema) {
190
+ return {
191
+ description: schema.description ?? null,
192
+ types: schema.nodes.map((node) => {
193
+ const result = { type: node.type };
194
+ if (node.description) result.description = node.description;
195
+ if (node.connections.parent) {
196
+ result.parent = `${node.connections.parent.relation} \u2192 ${node.connections.parent.target_node}`;
197
+ }
198
+ const connections = [
199
+ ...node.connections.required.map((c) => `${c.relation} \u2192 ${c.target_node} (required)`),
200
+ ...node.connections.suggested.map((c) => `${c.relation} \u2192 ${c.target_node}`)
201
+ ];
202
+ if (connections.length > 0) result.connections = connections;
203
+ if (node.attributes && node.attributes.length > 0) {
204
+ result.attributes = Object.fromEntries(
205
+ node.attributes.map((a) => {
206
+ const values = (a.values ?? []).map(
207
+ (v) => v.description ? { label: v.label, description: v.description } : v.label
208
+ );
209
+ const attrDetail = { values };
210
+ if (a.description) attrDetail.description = a.description;
211
+ const hasDesc = !!a.description;
212
+ const hasValueDescs = values.some((v) => typeof v !== "string");
213
+ if (!hasDesc && !hasValueDescs) {
214
+ return [a.name, (a.values ?? []).map((v) => v.label)];
215
+ }
216
+ return [a.name, attrDetail];
217
+ })
218
+ );
219
+ }
220
+ return result;
221
+ })
222
+ };
223
+ }
224
+ function registerGetSchema(server2, client2) {
225
+ server2.registerTool(
226
+ "naumu_get_schema",
227
+ {
228
+ title: "Get Graph Schema",
229
+ annotations: { readOnlyHint: true },
230
+ description: "Get the schema definition for a knowledge graph. Returns node types, their allowed attributes (with valid values), allowed relationships, and any descriptions authored on types/attributes/values. Call this before classifying a new node into a type, before picking an attribute value, or before extending the schema \u2014 descriptions are short, contrastive notes from the schema author explaining what each type/attribute/value is for and how it differs from similar-sounding ones.",
231
+ inputSchema: z3.object({
232
+ graphId: z3.string().describe("The graph ID")
233
+ })
234
+ },
235
+ async ({ graphId }) => {
236
+ const data = await client2.get(`/api/graphs/${graphId}/schema`);
237
+ const schema = JSON.parse(data.definition);
238
+ const formatted = formatSchema(schema);
239
+ let spaceDescription = null;
240
+ try {
241
+ const summary = await client2.get(`/api/graphs/${graphId}/summary`);
242
+ spaceDescription = summary.description ?? summary.summary ?? null;
243
+ } catch {
244
+ }
245
+ const out = { ...formatted, space_description: spaceDescription };
246
+ return {
247
+ content: [{ type: "text", text: JSON.stringify(out, null, 2) }]
248
+ };
249
+ }
250
+ );
251
+ }
252
+
253
+ // ../mcp-core/src/tools/update-schema.ts
254
+ import { z as z4 } from "zod";
255
+ var ConnectionSchema = z4.object({
256
+ relation: z4.string().describe("UPPER_SNAKE_CASE relation name (e.g. WORKS_AT, BUILT_BY, BELONGS_TO)"),
257
+ target_node: z4.string().optional().describe("Target type name. Omit when polymorphic=true."),
258
+ polymorphic: z4.boolean().optional().describe("Set true when this relation can target multiple types.")
45
259
  });
46
- // Bot keys (`nmu_bot_*`) get a filtered tool surface — the backend computes
47
- // the allowed set from the Identity's `allowedTools`. User keys keep the full
48
- // surface (no per-Identity scoping applies).
49
- const isBotKey = apiKey.startsWith(BOT_KEY_PREFIX);
50
- if (isBotKey) {
51
- try {
52
- const manifest = (await client.get('/api/identities/me/mcp-tool-manifest'));
53
- registerNamedTools(server, client, manifest.mcpTools);
54
- console.error(`[mcp] connected to ${apiUrl} as bot identity ${manifest.identityId} (${manifest.intelligenceKind}); ${manifest.mcpTools.length} tool(s) registered`);
260
+ var AttributeValueSchema = z4.object({
261
+ label: z4.string().describe("Display label for this enum value"),
262
+ color: z4.string().optional().describe('Optional hex color (e.g. "#ff5722")'),
263
+ description: z4.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won \u2014 deal signed and revenue committed"). Encouraged when the label alone is ambiguous.')
264
+ });
265
+ var AttributeSchema = z4.object({
266
+ name: z4.string().describe('Attribute key (e.g. "stage", "status", "category")'),
267
+ type: z4.enum(["select", "multiselect", "string", "number", "date"]).optional().describe("Attribute type. Defaults to select if omitted."),
268
+ values: z4.array(AttributeValueSchema).describe("Allowed enum values for select/multiselect; pass [] for string/number/date."),
269
+ description: z4.string().optional().describe("Short note explaining what this attribute captures and how it differs from similarly-named attributes elsewhere in the schema. Strongly encouraged.")
270
+ });
271
+ var NodeTypeSchema = z4.object({
272
+ type: z4.string().describe("Type name in PascalCase (e.g. Company, Person, Feature, Document)"),
273
+ connections: z4.object({
274
+ parent: ConnectionSchema.optional().describe("Optional parent relation (this type nests under another via this connection)."),
275
+ required: z4.array(ConnectionSchema).default([]).describe("Required outgoing connections to other types."),
276
+ suggested: z4.array(ConnectionSchema).default([]).describe("Suggested-but-optional outgoing connections.")
277
+ }),
278
+ attributes: z4.array(AttributeSchema).optional().describe("Type-level attributes for instances of this type."),
279
+ defaultVisibility: z4.enum(["restricted", "internal", "open"]).optional().describe(
280
+ 'Default visibility for NEW nodes of this type when no explicit visibility is passed on creation. Does NOT retroactively change visibility on existing nodes. "open" = visible to anyone with the space link (including non-members), "internal" = visible to all space members, "restricted" = only members explicitly granted access. Omit to leave the type unset \u2014 it then falls back to the space-level defaultVisibility.'
281
+ ),
282
+ color: z4.string().optional().describe("Optional hex color for instances of this type."),
283
+ description: z4.string().optional().describe('Short one-sentence description of what this type represents AND how it differs from semantically similar types (e.g. "External entity delivering services on contract \u2014 distinct from Organization which is any legal entity"). Strongly encouraged on every type. Future agents rely on this when classifying a new node into one of several similar-sounding types.')
284
+ });
285
+ var SchemaDefinitionSchema = z4.object({
286
+ description: z4.string().optional().describe("Schema-level description / domain summary."),
287
+ nodes: z4.array(NodeTypeSchema).describe("All node types in the schema.")
288
+ });
289
+ function registerUpdateSchema(server2, client2) {
290
+ server2.registerTool(
291
+ "naumu_update_schema",
292
+ {
293
+ title: "Update Graph Schema",
294
+ annotations: { destructiveHint: true },
295
+ description: "Replace the graph schema with a new full definition. STRUCTURAL RULES (load-bearing): (1) HIERARCHICAL with a single root \u2014 exactly ONE type has no parent; every other type MUST declare a parent. (2) Each parent / required / suggested connection is XOR: ONE concrete target_node OR polymorphic=true \u2014 never both, never a list of multiple targets. (3) DEFAULT TO CONCRETE target_node. Polymorphic is the ESCAPE HATCH \u2014 reserve for genuinely cross-cutting concepts like Comment or Tag (entities that validly attach to many distinct types). If you find yourself making most parents polymorphic, you are avoiding the design work \u2014 pick concrete relationships instead. If you can't pick a concrete parent, you may be missing a type \u2014 add the missing type first. (4) Same-type nesting is implicit: a node can be placed under another of its same type using the existing parent relation \u2014 never add a self-relation just to enable nesting. (5) Always provide `description` on every node type, attribute, and select value \u2014 one short, contrastive sentence (what it IS and what it is NOT vs sibling types/attrs/values). This is the single strongest disambiguation signal for future agents classifying nodes. Use to bootstrap an empty graph (cold-start) or fully replace mid-build (call naumu_get_schema first, send the FULL new schema \u2014 anything you omit is removed). Conventions: type names PascalCase (Company, Person, Feature), relation names UPPER_SNAKE_CASE (WORKS_AT, BELONGS_TO). Bias toward general types \u2014 refine via attributes or nested children, not type proliferation.",
296
+ inputSchema: z4.object({
297
+ graphId: z4.string().describe("The graph ID"),
298
+ schema: SchemaDefinitionSchema
299
+ })
300
+ },
301
+ async ({ graphId, schema }) => {
302
+ const data = await client2.post(`/api/graphs/${graphId}/schema`, {
303
+ definition: schema
304
+ });
305
+ return {
306
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
307
+ };
308
+ }
309
+ );
310
+ }
311
+
312
+ // ../mcp-core/src/tools/add-node-type.ts
313
+ import { z as z5 } from "zod";
314
+ var ConnectionSchema2 = z5.object({
315
+ relation: z5.string().describe("UPPER_SNAKE_CASE relation name (WORKS_AT, BELONGS_TO)."),
316
+ target_node: z5.string().optional().describe("Target type name. Omit when polymorphic=true."),
317
+ polymorphic: z5.boolean().optional().describe("True when this relation can target many types.")
318
+ });
319
+ var AttributeValueSchema2 = z5.object({
320
+ label: z5.string(),
321
+ color: z5.string().optional(),
322
+ description: z5.string().optional().describe('Short note distinguishing this value from sibling values (e.g. for status="closed-won", "Deal signed and revenue committed"). Encouraged when the label alone is ambiguous.')
323
+ });
324
+ var AttributeSchema2 = z5.object({
325
+ name: z5.string(),
326
+ type: z5.enum(["select", "multiselect", "string", "number", "date"]).optional(),
327
+ values: z5.array(AttributeValueSchema2).default([]),
328
+ description: z5.string().optional().describe('Short note explaining what this attribute captures and how it differs from similarly-named attributes on other types (e.g. "stage on Feature" vs "stage on Deal"). Strongly encouraged.')
329
+ });
330
+ function registerAddNodeType(server2, client2) {
331
+ server2.registerTool(
332
+ "naumu_add_node_type",
333
+ {
334
+ title: "Add Node Type to Schema",
335
+ annotations: { destructiveHint: true },
336
+ description: 'Add one new node type to the schema. Cheaper than naumu_update_schema. STRUCTURAL RULES: (1) Schemas are hierarchical with a single root. If schema is empty, the first type IS the root \u2014 omit `parent`. Every subsequent type MUST set `parent`. (2) DEFAULT TO CONCRETE \u2014 pass {relation, target_node: <existing type>}. Polymorphic is the ESCAPE HATCH \u2014 use {relation, polymorphic: true} ONLY for genuinely cross-cutting concepts like Comment or Tag (types that validly attach to many distinct parents). If you can\'t pick a concrete parent, you may be missing a type \u2014 add the missing type first instead of falling back to polymorphic. (3) Required/suggested connections also each take ONE concrete target_node OR polymorphic \u2014 never a list. Default concrete there too. (4) Type name PascalCase (Company, Person, Feature). Relation names UPPER_SNAKE_CASE (BELONGS_TO, WORKS_AT). (5) ALWAYS provide `description` \u2014 one short sentence that says what this type is AND what it is NOT relative to semantically close types (e.g. "External entity that delivers services on contract; distinct from Organization which is any legal entity"). This is the single strongest signal future agents have when classifying a node into one of several similar-sounding types. Returns an error if the type already exists \u2014 use naumu_add_attribute / naumu_add_connection to extend it.',
337
+ inputSchema: z5.object({
338
+ graphId: z5.string(),
339
+ type: z5.string().describe("PascalCase type name"),
340
+ description: z5.string().optional().describe(
341
+ "Short one-sentence description of what this type represents and how it differs from sibling types. Contrastive (say what it IS and what it is NOT) is most useful. Strongly encouraged on every new type \u2014 agents reading the schema later rely on this to classify nodes."
342
+ ),
343
+ parent: ConnectionSchema2.optional().describe("Optional parent connection \u2014 sets this type as a child of another type."),
344
+ required: z5.array(ConnectionSchema2).optional(),
345
+ suggested: z5.array(ConnectionSchema2).optional(),
346
+ attributes: z5.array(AttributeSchema2).optional(),
347
+ color: z5.string().optional(),
348
+ defaultVisibility: z5.enum(["restricted", "internal", "open"]).optional().describe(
349
+ 'Default visibility for NEW nodes of this type when no explicit visibility is passed on creation. Does NOT retroactively change existing nodes. "open" = visible to anyone with the space link, "internal" = visible to all space members, "restricted" = only members explicitly granted access. Omit to fall back to the space-level defaultVisibility.'
350
+ )
351
+ })
352
+ },
353
+ async ({ graphId, type, description, parent, required, suggested, attributes, color, defaultVisibility }) => {
354
+ const current = await client2.get(`/api/graphs/${graphId}/schema`);
355
+ const schema = current.definition ? JSON.parse(current.definition) : { nodes: [] };
356
+ if (schema.nodes.some((n) => n.type === type)) {
357
+ return {
358
+ content: [{ type: "text", text: JSON.stringify({ error: `Node type "${type}" already exists. Use naumu_add_attribute / naumu_add_connection to extend it.` }, null, 2) }]
359
+ };
360
+ }
361
+ const newNode = {
362
+ type,
363
+ connections: {
364
+ required: required ?? [],
365
+ suggested: suggested ?? [],
366
+ ...parent ? { parent } : {}
367
+ },
368
+ ...attributes && attributes.length > 0 ? { attributes } : {},
369
+ ...color ? { color } : {},
370
+ ...description ? { description } : {},
371
+ ...defaultVisibility ? { defaultVisibility } : {}
372
+ };
373
+ schema.nodes.push(newNode);
374
+ await client2.post(`/api/graphs/${graphId}/schema`, { definition: schema });
375
+ return {
376
+ content: [{ type: "text", text: JSON.stringify({ message: `Added node type "${type}"`, total_types: schema.nodes.length }, null, 2) }]
377
+ };
378
+ }
379
+ );
380
+ }
381
+
382
+ // ../mcp-core/src/tools/add-connection.ts
383
+ import { z as z6 } from "zod";
384
+ function registerAddConnection(server2, client2) {
385
+ server2.registerTool(
386
+ "naumu_add_connection",
387
+ {
388
+ title: "Add Connection to Node Type",
389
+ annotations: { destructiveHint: true },
390
+ description: 'Add one connection from an existing node type to another. `kind`: "parent" (sets/replaces \u2014 every non-root type needs exactly one parent; only ONE type in the whole schema has no parent), "required" (must exist on instances), "suggested" (optional). Each connection is XOR: ONE concrete target_node OR polymorphic=true \u2014 never both. DEFAULT TO CONCRETE target_node. Polymorphic is the ESCAPE HATCH \u2014 reserve for genuinely cross-cutting relations (e.g. a Tag-style relation that validly applies to many distinct types). If you can\'t pick a concrete target, you may be missing a type \u2014 add it first instead of falling back to polymorphic. Same-type nesting (e.g. Topic under Topic) does NOT need a new connection \u2014 use the existing parent relation. Relation name UPPER_SNAKE_CASE.',
391
+ inputSchema: z6.object({
392
+ graphId: z6.string(),
393
+ source_type: z6.string().describe("Existing node type to add the connection to."),
394
+ relation: z6.string().describe("UPPER_SNAKE_CASE relation name."),
395
+ target_node: z6.string().optional().describe("Target type. Omit when polymorphic=true."),
396
+ polymorphic: z6.boolean().optional(),
397
+ kind: z6.enum(["required", "suggested", "parent"]).default("suggested")
398
+ })
399
+ },
400
+ async ({ graphId, source_type, relation, target_node, polymorphic, kind }) => {
401
+ const current = await client2.get(`/api/graphs/${graphId}/schema`);
402
+ const schema = current.definition ? JSON.parse(current.definition) : { nodes: [] };
403
+ const node = schema.nodes.find((n) => n.type === source_type);
404
+ if (!node) {
405
+ return {
406
+ content: [{ type: "text", text: JSON.stringify({ error: `Node type "${source_type}" not found. Add it first via naumu_add_node_type.` }, null, 2) }]
407
+ };
408
+ }
409
+ const conn = { relation, ...polymorphic ? { polymorphic: true } : {}, ...target_node ? { target_node } : {} };
410
+ if (!node.connections) node.connections = { required: [], suggested: [] };
411
+ if (kind === "parent") {
412
+ node.connections.parent = conn;
413
+ } else {
414
+ const list = node.connections[kind] ?? [];
415
+ const exists = list.some((c) => c.relation === relation && c.target_node === target_node && Boolean(c.polymorphic) === Boolean(polymorphic));
416
+ if (exists) {
417
+ return {
418
+ content: [{ type: "text", text: JSON.stringify({ message: `Connection already exists on "${source_type}", no change.` }, null, 2) }]
419
+ };
420
+ }
421
+ list.push(conn);
422
+ node.connections[kind] = list;
423
+ }
424
+ await client2.post(`/api/graphs/${graphId}/schema`, { definition: schema });
425
+ return {
426
+ content: [{ type: "text", text: JSON.stringify({ message: `Added ${kind} connection ${relation}${target_node ? ` \u2192 ${target_node}` : " (polymorphic)"} on "${source_type}".` }, null, 2) }]
427
+ };
428
+ }
429
+ );
430
+ }
431
+
432
+ // ../mcp-core/src/tools/add-attribute.ts
433
+ import { z as z7 } from "zod";
434
+ function registerAddAttribute(server2, client2) {
435
+ server2.registerTool(
436
+ "naumu_add_attribute",
437
+ {
438
+ title: "Add Attribute to Node Type",
439
+ annotations: { destructiveHint: true },
440
+ description: 'Add or extend an attribute on an existing node type. If the attribute name doesn\'t exist, it is created. If it exists and is a select/multiselect, new values are merged in (existing values kept). Use type "select"/"multiselect" with values; "string"/"number"/"date" for free-form fields (pass values: [] \u2014 string/number/date have no enum values). Example date attribute: { name: "due_date", type: "date", values: [], description: "Target completion date (single day or range)" }. Date values on nodes are written via naumu_update_node as either an ISO date string "YYYY-MM-DD" (single day) or an object { start, end? } with ISO date strings (inclusive range). Strongly encouraged to provide `description` on the attribute and on each value \u2014 short, contrastive notes (e.g. attribute "stage \u2014 sales funnel position; distinct from status which captures health/blockers", value "closed-won \u2014 deal signed and revenue committed"). Descriptions are the single strongest signal future agents use when picking which attribute/value to set.',
441
+ inputSchema: z7.object({
442
+ graphId: z7.string(),
443
+ node_type: z7.string().describe("Existing node type to add the attribute to."),
444
+ name: z7.string().describe('Attribute key (e.g. "stage", "status").'),
445
+ type: z7.enum(["select", "multiselect", "string", "number", "date"]).default("select"),
446
+ values: z7.array(
447
+ z7.object({
448
+ label: z7.string(),
449
+ color: z7.string().optional(),
450
+ description: z7.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won \u2014 deal signed and revenue committed"). Encouraged when the label alone is ambiguous.')
451
+ })
452
+ ).default([]),
453
+ description: z7.string().optional().describe('Short note explaining what this attribute captures and how it differs from similarly-named attributes elsewhere in the schema (e.g. "Sales funnel position; differs from `status` which captures health/blockers"). Strongly encouraged.')
454
+ })
455
+ },
456
+ async ({ graphId, node_type, name, type, values, description }) => {
457
+ const current = await client2.get(`/api/graphs/${graphId}/schema`);
458
+ const schema = current.definition ? JSON.parse(current.definition) : { nodes: [] };
459
+ const node = schema.nodes.find((n) => n.type === node_type);
460
+ if (!node) {
461
+ return {
462
+ content: [{ type: "text", text: JSON.stringify({ error: `Node type "${node_type}" not found. Add it first via naumu_add_node_type.` }, null, 2) }]
463
+ };
464
+ }
465
+ node.attributes = node.attributes ?? [];
466
+ const existing = node.attributes.find((a) => a.name === name);
467
+ let action;
468
+ if (!existing) {
469
+ node.attributes.push({ name, type, values, ...description ? { description } : {} });
470
+ action = `Created attribute "${name}" (${type}) with ${values.length} value(s).`;
471
+ } else {
472
+ const existingLabels = new Set((existing.values ?? []).map((v) => v.label));
473
+ const added = values.filter((v) => !existingLabels.has(v.label));
474
+ existing.values = [...existing.values ?? [], ...added];
475
+ if (description && !existing.description) existing.description = description;
476
+ action = added.length > 0 ? `Extended attribute "${name}" with ${added.length} new value(s); kept ${existingLabels.size} existing.` : `Attribute "${name}" already had all proposed values; no change.`;
477
+ }
478
+ await client2.post(`/api/graphs/${graphId}/schema`, { definition: schema });
479
+ return {
480
+ content: [{ type: "text", text: JSON.stringify({ message: action, node_type }, null, 2) }]
481
+ };
482
+ }
483
+ );
484
+ }
485
+
486
+ // ../mcp-core/src/tools/search.ts
487
+ import { z as z8 } from "zod";
488
+ function registerSearch(server2, client2) {
489
+ server2.registerTool(
490
+ "naumu_search",
491
+ {
492
+ title: "Search Graph",
493
+ annotations: { readOnlyHint: true },
494
+ description: 'Hybrid search over graph nodes. Combines exact-token text matching (good for UUIDs, proper nouns, specific labels) with semantic similarity (good for paraphrase and meaning), then fuses both rankings with Reciprocal Rank Fusion. Returns the top matches with a `matchedVia` tag \u2014 `both` is the highest-confidence signal, then `semantic`, then `text`. Use `naumu_filter` for structured queries by type and attributes (e.g. "all in-progress Tasks"). Tip: include both synonyms ("authentication login SSO") and exact tokens you remember in the same query \u2014 the fusion handles both.',
495
+ inputSchema: z8.object({
496
+ graphId: z8.string().describe("The graph ID"),
497
+ query: z8.string().describe(
498
+ 'Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa Twitter handle").'
499
+ ),
500
+ limit: z8.number().optional().default(20).describe("Max results to return (default 20, max 200). Adaptive cutoff may return fewer when the top match is weak."),
501
+ nodeTypes: z8.array(z8.string()).optional().describe('Filter to specific node types (e.g. ["Feature", "Pain"])')
502
+ })
503
+ },
504
+ async ({ graphId, query, limit, nodeTypes }) => {
505
+ const params = new URLSearchParams();
506
+ params.set("q", query);
507
+ if (limit) params.set("limit", String(limit));
508
+ if (nodeTypes?.length) params.set("nodeTypes", nodeTypes.join(","));
509
+ const data = await client2.get(`/api/graphs/${graphId}/search/hybrid?${params.toString()}`);
510
+ return {
511
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
512
+ };
513
+ }
514
+ );
515
+ }
516
+
517
+ // ../mcp-core/src/tools/filter.ts
518
+ import { z as z9 } from "zod";
519
+ function registerFilter(server2, client2) {
520
+ server2.registerTool(
521
+ "naumu_filter",
522
+ {
523
+ title: "Filter Graph Nodes",
524
+ annotations: { readOnlyHint: true },
525
+ description: 'Filter nodes by type and attributes with deterministic, complete results. Use this for structured queries like "all in-progress Tasks" or "Bugs not yet resolved." Unlike search tools, this returns every matching node (up to the limit) \u2014 no semantic ranking, no missed results. Results are sorted by sortKey or recency.',
526
+ inputSchema: z9.object({
527
+ graphId: z9.string().describe("The graph ID"),
528
+ nodeTypes: z9.array(z9.string()).optional().describe('Filter to specific node types (e.g. ["Task", "Bug"])'),
529
+ includeAttributes: z9.record(z9.array(z9.string())).optional().describe(
530
+ 'Only include nodes where attribute matches one of the values. Example: {"Status": ["Todo", "In Progress"]}'
531
+ ),
532
+ excludeAttributes: z9.record(z9.array(z9.string())).optional().describe(
533
+ 'Exclude nodes where attribute matches any of the values. Example: {"Status": ["Done", "Wont do"]}'
534
+ ),
535
+ sortBy: z9.enum(["sortKey", "updatedAt", "label"]).optional().default("sortKey").describe('Sort order: "sortKey" (default), "updatedAt" (most recent first), or "label" (alphabetical)'),
536
+ limit: z9.number().optional().default(50).describe("Max results to return (default 50, max 200)")
537
+ })
538
+ },
539
+ async ({ graphId, nodeTypes, includeAttributes, excludeAttributes, sortBy, limit }) => {
540
+ const params = new URLSearchParams();
541
+ if (nodeTypes?.length) params.set("nodeTypes", nodeTypes.join(","));
542
+ if (sortBy) params.set("sortBy", sortBy);
543
+ if (limit) params.set("limit", String(limit));
544
+ if (includeAttributes) {
545
+ const parts = Object.entries(includeAttributes).map(([k, v]) => `${k}:${v.join(",")}`);
546
+ params.set("includeAttributes", parts.join(";"));
547
+ }
548
+ if (excludeAttributes) {
549
+ const parts = Object.entries(excludeAttributes).map(([k, v]) => `${k}:${v.join(",")}`);
550
+ params.set("excludeAttributes", parts.join(";"));
551
+ }
552
+ const data = await client2.get(`/api/graphs/${graphId}/filter?${params.toString()}`);
553
+ return {
554
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
555
+ };
556
+ }
557
+ );
558
+ }
559
+
560
+ // ../mcp-core/src/tools/get-node.ts
561
+ import { z as z10 } from "zod";
562
+ function registerGetNode(server2, client2) {
563
+ server2.registerTool(
564
+ "naumu_get_node",
565
+ {
566
+ title: "Get Node",
567
+ annotations: { readOnlyHint: true },
568
+ description: "Get a single node with all its properties and connections (incoming and outgoing edges).",
569
+ inputSchema: z10.object({
570
+ graphId: z10.string().describe("The graph ID"),
571
+ nodeId: z10.string().describe("The node ID")
572
+ })
573
+ },
574
+ async ({ graphId, nodeId }) => {
575
+ const data = await client2.get(`/api/graphs/${graphId}/nodes/${nodeId}`);
576
+ return {
577
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
578
+ };
579
+ }
580
+ );
581
+ }
582
+
583
+ // ../mcp-core/src/tools/add-node.ts
584
+ import { z as z11 } from "zod";
585
+ var NodeInput = z11.object({
586
+ label: z11.string().describe("Display name of the node"),
587
+ type: z11.string().describe("Node type from the graph schema (e.g. Feature, Pain, Metric)"),
588
+ content: z11.string().min(1).describe("Rich text content / description. REQUIRED \u2014 every node must explain what it is."),
589
+ attributes: z11.record(z11.string(), z11.unknown()).optional().describe("Additional key-value attributes")
590
+ });
591
+ function registerAddNode(server2, client2) {
592
+ server2.registerTool(
593
+ "naumu_add_node",
594
+ {
595
+ title: "Add Nodes (bulk)",
596
+ annotations: { destructiveHint: true },
597
+ description: 'Create 1\u201325 nodes in the knowledge graph in a single call. Keep batches small and atomic (5\u201325 nodes) so failures stay contained. Each node MUST include a non-empty `content` describing what it is. Returns one entry per input node with `{id, label, type, status: "created"}` \u2014 there is NO server-side dedup, every input becomes a node. Dedup is the caller\'s responsibility: BEFORE calling this tool, run `naumu_semantic_search` on each candidate label and skip/route to update if a result has high similarity (\u22650.78) and matching type. Warning: nodes are isolated until you connect them with `naumu_add_edge`. **Prefer `naumu_ask`** for general knowledge intake \u2014 it discovers and creates connections for you. Use this when you have a vetted, dedup-checked batch ready to insert.',
598
+ inputSchema: z11.object({
599
+ graphId: z11.string().describe("The graph ID"),
600
+ nodes: z11.array(NodeInput).min(1).max(25).describe("Batch of 1\u201325 nodes to create. Keep batches small for atomicity.")
601
+ })
602
+ },
603
+ async ({ graphId, nodes }) => {
604
+ const payload = nodes.map(({ label, type, content, attributes }) => {
605
+ const node = { label, type, content };
606
+ if (attributes) Object.assign(node, attributes);
607
+ return node;
608
+ });
609
+ const data = await client2.post(`/api/graphs/${graphId}/nodes`, { nodes: payload });
610
+ return {
611
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
612
+ };
613
+ }
614
+ );
615
+ }
616
+
617
+ // ../mcp-core/src/tools/update-node.ts
618
+ import { z as z12 } from "zod";
619
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
620
+ function isValidIsoDate(s) {
621
+ if (typeof s !== "string") return false;
622
+ if (!ISO_DATE_REGEX.test(s)) return false;
623
+ const [yearStr, monthStr, dayStr] = s.split("-");
624
+ const year = Number(yearStr);
625
+ const month = Number(monthStr);
626
+ const day = Number(dayStr);
627
+ const date = new Date(Date.UTC(year, month - 1, day));
628
+ if (Number.isNaN(date.getTime())) return false;
629
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
630
+ }
631
+ function isValidDateRange(value) {
632
+ if (value === null || typeof value !== "object") return false;
633
+ const candidate = value;
634
+ if (!isValidIsoDate(candidate.start)) return false;
635
+ if (candidate.end !== void 0) {
636
+ if (!isValidIsoDate(candidate.end)) return false;
637
+ if (candidate.end < candidate.start) return false;
638
+ }
639
+ return true;
640
+ }
641
+ function getAttributeMap(schema, nodeType) {
642
+ const map = /* @__PURE__ */ new Map();
643
+ const nodeDef = schema.nodes.find((n) => n.type === nodeType);
644
+ if (nodeDef?.attributes) {
645
+ for (const attr of nodeDef.attributes) {
646
+ map.set(attr.name.toLowerCase(), attr);
647
+ }
648
+ }
649
+ return map;
650
+ }
651
+ function validateDateAttribute(name, val) {
652
+ const rejectionMessage = `Date attribute "${name}" expects null, an ISO date string (YYYY-MM-DD), or { start, end? } with ISO dates.`;
653
+ if (val === null) return { ok: true, value: null };
654
+ if (typeof val === "string") {
655
+ if (isValidIsoDate(val)) return { ok: true, value: val };
656
+ return { ok: false, error: rejectionMessage };
657
+ }
658
+ if (typeof val === "object" && val !== null) {
659
+ if (isValidDateRange(val)) {
660
+ const range = val;
661
+ const normalized = range.end === void 0 || range.end === range.start ? { start: range.start } : { start: range.start, end: range.end };
662
+ return { ok: true, value: normalized };
663
+ }
664
+ return { ok: false, error: rejectionMessage };
665
+ }
666
+ return { ok: false, error: rejectionMessage };
667
+ }
668
+ function registerUpdateNode(server2, client2) {
669
+ server2.registerTool(
670
+ "naumu_update_node",
671
+ {
672
+ title: "Update Node",
673
+ annotations: { destructiveHint: true },
674
+ description: 'Update properties of an existing node. Only the provided fields will be changed. Good for simple attribute changes like setting a status, priority, or due date. For content or structural changes, consider `naumu_ask` instead \u2014 it understands the full graph context and can propagate updates to related nodes. Attribute keys AND values must match the schema for the node type. Use naumu_get_schema to check valid attribute names, types, and values before updating. Date attributes accept either null (to clear), an ISO date string "YYYY-MM-DD" (single day), or { start: "YYYY-MM-DD", end?: "YYYY-MM-DD" } (inclusive range). Example: { "due_date": { "start": "2026-05-21", "end": "2026-05-23" } } or { "due_date": "2026-05-21" } or { "due_date": null }. Select attributes accept the value label as a string.',
675
+ inputSchema: z12.object({
676
+ graphId: z12.string().describe("The graph ID"),
677
+ nodeId: z12.string().describe("The node ID to update"),
678
+ label: z12.string().optional().describe("New display name"),
679
+ type: z12.string().optional().describe("New node type"),
680
+ content: z12.string().optional().describe("New content / description"),
681
+ attributes: z12.record(z12.string(), z12.unknown()).optional().describe('Schema-defined attributes to set. Examples: {"Status": "In Progress"}, {"due_date": {"start": "2026-05-21", "end": "2026-05-23"}}, {"due_date": "2026-05-21"}, {"due_date": null}.')
682
+ })
683
+ },
684
+ async ({ graphId, nodeId, label, type, content, attributes }) => {
685
+ const body = {};
686
+ if (label !== void 0) body.label = label;
687
+ if (type !== void 0) body.type = type;
688
+ if (content !== void 0) body.content = content;
689
+ if (attributes && Object.keys(attributes).length > 0) {
690
+ const node = await client2.get(`/api/graphs/${graphId}/nodes/${nodeId}`);
691
+ const nodeType = type ?? node.type;
692
+ const schemaRes = await client2.get(`/api/graphs/${graphId}/schema`);
693
+ const schema = JSON.parse(schemaRes.definition);
694
+ const attrMap = getAttributeMap(schema, nodeType);
695
+ const errors = [];
696
+ for (const [key, val] of Object.entries(attributes)) {
697
+ const normalized = key.toLowerCase();
698
+ const schemaDef = attrMap.get(normalized);
699
+ if (!schemaDef) {
700
+ const validNames = [...attrMap.values()].map((a) => a.name);
701
+ errors.push(
702
+ `Unknown attribute "${key}" for type "${nodeType}". Valid attributes: ${validNames.length > 0 ? validNames.join(", ") : "none"}.`
703
+ );
704
+ continue;
705
+ }
706
+ if (schemaDef.type === "date") {
707
+ const result = validateDateAttribute(schemaDef.name, val);
708
+ if (!result.ok) {
709
+ errors.push(result.error);
710
+ continue;
711
+ }
712
+ body[normalized] = result.value;
713
+ continue;
714
+ }
715
+ const validValues = schemaDef.values.map((v) => v.label);
716
+ if (typeof val === "string" && validValues.length > 0 && !validValues.includes(val)) {
717
+ errors.push(
718
+ `Invalid value "${val}" for attribute "${schemaDef.name}" on type "${nodeType}". Valid values: ${validValues.join(", ")}.`
719
+ );
720
+ continue;
721
+ }
722
+ body[normalized] = val;
723
+ }
724
+ if (errors.length > 0) {
725
+ return {
726
+ content: [
727
+ {
728
+ type: "text",
729
+ text: `Attribute validation failed:
730
+ ${errors.join("\n")}
731
+
732
+ Use naumu_get_schema to check valid attributes and values for this node type.`
733
+ }
734
+ ],
735
+ isError: true
736
+ };
737
+ }
738
+ }
739
+ const data = await client2.patch(`/api/graphs/${graphId}/nodes/${nodeId}`, body);
740
+ return {
741
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
742
+ };
743
+ }
744
+ );
745
+ }
746
+
747
+ // ../mcp-core/src/tools/add-edge.ts
748
+ import { z as z13 } from "zod";
749
+ var EdgeInput = z13.object({
750
+ source: z13.string().describe("Source node ID"),
751
+ target: z13.string().describe("Target node ID"),
752
+ label: z13.string().describe("Relationship type (e.g. RELATES_TO, SOLVES, TRACKS)"),
753
+ isParent: z13.boolean().optional().describe("Whether this is a parent relationship")
754
+ });
755
+ function registerAddEdge(server2, client2) {
756
+ server2.registerTool(
757
+ "naumu_add_edge",
758
+ {
759
+ title: "Add Edges (bulk)",
760
+ annotations: { destructiveHint: true },
761
+ description: "Create 1\u201325 relationships (edges) between existing nodes in a single call. Keep batches small and atomic (5\u201325 edges) so failures stay contained. Typical workflow after a bulk import: create nodes with `naumu_add_node`, then wire them up with batched `naumu_add_edge` calls. **Direction matters and must match the schema.** Each edge's `(source.type, label, target.type)` tuple should appear in the schema's `connections` or `parent` for the source type. When the backend runs with `STRICT_EDGE_VALIDATION=true`, invalid tuples are rejected immediately with `error: invalid_edge` (the response includes `details.allowed_targets_for_relation` and a `hint` for routing the call); otherwise the edge persists and surfaces later as an `invalid_connection_target` / `parent_mismatch` violation. Either way, check `naumu_get_schema` and flip / drop offending edges before calling. **Prefer `naumu_ask`** when you want the agent to discover the right connections itself.",
762
+ inputSchema: z13.object({
763
+ graphId: z13.string().describe("The graph ID"),
764
+ edges: z13.array(EdgeInput).min(1).max(25).describe("Batch of 1\u201325 edges to create. Keep batches small for atomicity.")
765
+ })
766
+ },
767
+ async ({ graphId, edges }) => {
768
+ const payload = edges.map((e) => ({
769
+ source: e.source,
770
+ target: e.target,
771
+ label: e.label,
772
+ isParent: e.isParent || false
773
+ }));
774
+ const data = await client2.post(`/api/graphs/${graphId}/edges`, { edges: payload });
775
+ return {
776
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
777
+ };
778
+ }
779
+ );
780
+ }
781
+
782
+ // ../mcp-core/src/tools/remove-node.ts
783
+ import { z as z14 } from "zod";
784
+ function registerRemoveNode(server2, client2) {
785
+ server2.registerTool(
786
+ "naumu_remove_node",
787
+ {
788
+ title: "Remove Node",
789
+ annotations: { destructiveHint: true },
790
+ description: "Low-level tool: delete a node and all its connections from the knowledge graph. This is a destructive operation. **Prefer `naumu_ask`** for removing knowledge \u2014 it understands the impact on the broader graph and can handle cascading changes. Use this only when you need precise, surgical deletion.",
791
+ inputSchema: z14.object({
792
+ graphId: z14.string().describe("The graph ID"),
793
+ nodeId: z14.string().describe("The node ID to delete")
794
+ })
795
+ },
796
+ async ({ graphId, nodeId }) => {
797
+ const data = await client2.del(`/api/graphs/${graphId}/nodes/${nodeId}`);
798
+ return {
799
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
800
+ };
801
+ }
802
+ );
803
+ }
804
+
805
+ // ../mcp-core/src/tools/remove-edge.ts
806
+ import { z as z15 } from "zod";
807
+ function registerRemoveEdge(server2, client2) {
808
+ server2.registerTool(
809
+ "naumu_remove_edge",
810
+ {
811
+ title: "Remove Edge",
812
+ annotations: { destructiveHint: true },
813
+ description: "Delete a single edge identified by `(source, target, label)` tuple. Does NOT delete the endpoint nodes \u2014 only the edge between them. System edges (HAS_NODE, HAS_THREAD, OWNS, etc.) are rejected with `error: system_edge_not_removable`. Parent edges (`isParent: true`) are removable but the response includes a warning that the child may now be orphaned \u2014 call `naumu_reparent` first if you want connectivity preserved. Response: `{deleted: 0 | 1, warnings: string[]}`. Use for correcting wrong-target edge mistakes; this is a recovery tool, not a routine one. **Prefer `naumu_ask`** when the structural intent is broader than removing one specific edge \u2014 `naumu_ask` can reason about the surrounding graph.",
814
+ inputSchema: z15.object({
815
+ graphId: z15.string().describe("The graph ID"),
816
+ source: z15.string().describe("Source node id of the edge to delete"),
817
+ target: z15.string().describe("Target node id of the edge to delete"),
818
+ label: z15.string().describe('Relation label of the edge to delete (e.g. "AUTHORED"). Case-insensitive; non-alphanum chars are normalized.')
819
+ })
820
+ },
821
+ async ({ graphId, source, target, label }) => {
822
+ const data = await client2.del(`/api/graphs/${graphId}/edges`, {
823
+ source,
824
+ target,
825
+ label
826
+ });
827
+ return {
828
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
829
+ };
55
830
  }
56
- catch (err) {
831
+ );
832
+ }
833
+
834
+ // ../mcp-core/src/tools/remove-edges-bulk.ts
835
+ import { z as z16 } from "zod";
836
+ var EdgeRef = z16.object({
837
+ source: z16.string().describe("Source node ID"),
838
+ target: z16.string().describe("Target node ID"),
839
+ label: z16.string().describe("Relation label of the edge to delete")
840
+ });
841
+ function registerRemoveEdgesBulk(server2, client2) {
842
+ server2.registerTool(
843
+ "naumu_remove_edges_bulk",
844
+ {
845
+ title: "Remove Edges (bulk)",
846
+ annotations: { destructiveHint: true },
847
+ description: "Delete 1\u2013100 edges in a single atomic call \u2014 all-or-none, same semantics as `naumu_add_node`. Does NOT delete endpoint nodes. **System edges** (HAS_NODE, HAS_THREAD, OWNS, etc.) are rejected \u2014 if any edge in the batch targets a system relation, the WHOLE batch is rejected with `error: system_edge_not_removable`. **Parent edges** (`isParent: true`) are removable but each parent removal contributes a warning to the response (`{deleted, warnings: string[]}`) \u2014 call `naumu_reparent` first if you want connectivity preserved. Use to fix multiple wrong-target edge mistakes in one shot. **Prefer `naumu_ask`** for broader structural cleanup that needs graph-wide reasoning.",
848
+ inputSchema: z16.object({
849
+ graphId: z16.string().describe("The graph ID"),
850
+ edges: z16.array(EdgeRef).min(1).max(100).describe("1\u2013100 edges to delete. Atomic per call \u2014 all succeed or none do.")
851
+ })
852
+ },
853
+ async ({ graphId, edges }) => {
854
+ const data = await client2.del(`/api/graphs/${graphId}/edges/bulk`, { edges });
855
+ return {
856
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
857
+ };
858
+ }
859
+ );
860
+ }
861
+
862
+ // ../mcp-core/src/tools/ask.ts
863
+ import { z as z17 } from "zod";
864
+ function registerAsk(server2, client2) {
865
+ server2.registerTool(
866
+ "naumu_ask",
867
+ {
868
+ title: "Ask AI",
869
+ annotations: { destructiveHint: true },
870
+ description: 'Send a message to the Naumu AI agent. Use for adding knowledge, making changes, asking questions, or reporting status updates. Always returns immediately with a threadId \u2014 the AI processes the request in the background. Depending on complexity, the agent may take anywhere from a few seconds to a few minutes to finish.\n\n**For reports and status updates** (e.g. "mark task X as done", "record a deployment"): fire and forget \u2014 no need to check the response.\n\n**For questions that need an answer**: after calling this tool, poll with `naumu_get_ai_thread` using the returned threadId until the AI response is ready.',
871
+ inputSchema: z17.object({
872
+ graphId: z17.string().describe("The graph ID to query against"),
873
+ question: z17.string().describe("Your message to the Naumu AI agent"),
874
+ threadId: z17.string().optional().describe("Thread ID for follow-up messages (omit for new conversation)")
875
+ })
876
+ },
877
+ async ({ graphId, question, threadId }) => {
878
+ if (!threadId) {
879
+ const thread = await client2.post("/api/threads", { graphId });
880
+ threadId = thread.id;
881
+ }
882
+ await client2.post(`/api/threads/${threadId}/messages`, {
883
+ content: question,
884
+ async: true
885
+ });
886
+ return {
887
+ content: [
888
+ {
889
+ type: "text",
890
+ text: JSON.stringify({ threadId, status: "processing" }, null, 2)
891
+ }
892
+ ]
893
+ };
894
+ }
895
+ );
896
+ }
897
+
898
+ // ../mcp-core/src/tools/get-ai-thread.ts
899
+ import { z as z18 } from "zod";
900
+ function registerGetAiThread(server2, client2) {
901
+ server2.registerTool(
902
+ "naumu_get_ai_thread",
903
+ {
904
+ title: "Get AI Thread",
905
+ annotations: { readOnlyHint: true },
906
+ description: 'Retrieve messages from an AI conversation thread. Use to read the AI response after a `naumu_ask` call.\n\nThe last AI message has a `status` field: `"processing"` means the AI is still working, `"complete"` means the response is ready. If still processing, wait a few seconds and try again.',
907
+ inputSchema: z18.object({
908
+ threadId: z18.string().describe("The thread ID returned by naumu_ask")
909
+ })
910
+ },
911
+ async ({ threadId }) => {
912
+ const data = await client2.get(`/api/threads/${threadId}/messages`);
913
+ return {
914
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
915
+ };
916
+ }
917
+ );
918
+ }
919
+
920
+ // ../mcp-core/src/tools/traverse.ts
921
+ import { z as z19 } from "zod";
922
+ function registerTraverse(server2, client2) {
923
+ server2.registerTool(
924
+ "naumu_traverse",
925
+ {
926
+ title: "Traverse Knowledge Graph",
927
+ annotations: { readOnlyHint: true },
928
+ description: 'Query the graph structurally using natural language. A sub-LLM generates and executes Cypher queries. Use for structural questions like "tasks without a parent", "features with most tasks", "orphan nodes", "everything within 2 hops of X", or temporal filters like "nodes updated this week". For meaning-based search, use `naumu_search` instead.',
929
+ inputSchema: z19.object({
930
+ graphId: z19.string().describe("The graph ID"),
931
+ query: z19.string().describe(
932
+ `Natural language description of the structural pattern to find (e.g. "tasks without a parent", "features that don't resolve any pain")`
933
+ )
934
+ })
935
+ },
936
+ async ({ graphId, query }) => {
937
+ const data = await client2.post(`/api/graphs/${graphId}/traverse`, { query });
938
+ return {
939
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
940
+ };
941
+ }
942
+ );
943
+ }
944
+
945
+ // ../mcp-core/src/tools/get-view.ts
946
+ import { z as z20 } from "zod";
947
+ function registerGetView(server2, client2) {
948
+ server2.registerTool(
949
+ "naumu_get_view",
950
+ {
951
+ title: "Get View",
952
+ annotations: { readOnlyHint: true },
953
+ description: "Fetch a saved Naumu view's configuration plus a one-line natural-language summary of its filters. Call this BEFORE naumu_list_view_nodes whenever you encounter a view URL \u2014 the summary tells you what data the view returns so you can decide whether to page through it. Returns: id, name, description, summary, filters, and table column config.",
954
+ inputSchema: z20.object({
955
+ graphId: z20.string().describe("The graph (space) ID. From naumu.ai URLs this is the value after /spaces/."),
956
+ viewId: z20.string().describe('The view ID (typically prefixed with "view_").')
957
+ })
958
+ },
959
+ async ({ graphId, viewId }) => {
960
+ const data = await client2.get(`/api/graphs/${graphId}/views/${viewId}`);
961
+ return {
962
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
963
+ };
964
+ }
965
+ );
966
+ }
967
+
968
+ // ../mcp-core/src/tools/list-view-nodes.ts
969
+ import { z as z21 } from "zod";
970
+ function registerListViewNodes(server2, client2) {
971
+ server2.registerTool(
972
+ "naumu_list_view_nodes",
973
+ {
974
+ title: "List View Nodes",
975
+ annotations: { readOnlyHint: true },
976
+ description: 'List nodes that match a saved view. Cursor-paginated. Pair with naumu_get_view first to read the view\'s filters and decide what payload size you need.\n\nResponse shape: { nodes, totalCount, nextCursor, hasMore }. Use totalCount to know how many results exist without paging through them. Use hasMore (boolean) to decide whether to fetch the next page; pass nextCursor back as `cursor` to do so.\n\nfields parameter:\n- "summary" (default): {id, label, type} \u2014 best for browsing.\n- "id": {id} only \u2014 use when you need to count or iterate cheaply.\n- "full": full node payload with all attributes \u2014 use when you need every property.\n\nDefault page size 25, max 100.',
977
+ inputSchema: z21.object({
978
+ graphId: z21.string().describe("The graph (space) ID."),
979
+ viewId: z21.string().describe("The view ID."),
980
+ cursor: z21.string().optional().describe("Opaque cursor from a previous response's nextCursor. Omit for the first page."),
981
+ limit: z21.number().int().min(1).max(100).optional().describe("Page size, default 25, max 100."),
982
+ fields: z21.enum(["id", "summary", "full"]).optional().describe('How much detail per node. Default "summary".')
983
+ })
984
+ },
985
+ async ({ graphId, viewId, cursor, limit, fields }) => {
986
+ const params = new URLSearchParams();
987
+ if (cursor) params.set("cursor", cursor);
988
+ if (limit !== void 0) params.set("limit", String(limit));
989
+ if (fields) params.set("fields", fields);
990
+ const qs = params.toString();
991
+ const path = `/api/graphs/${graphId}/views/${viewId}/nodes${qs ? `?${qs}` : ""}`;
992
+ const data = await client2.get(path);
993
+ return {
994
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
995
+ };
996
+ }
997
+ );
998
+ }
999
+
1000
+ // ../mcp-core/src/tools/list-canvases.ts
1001
+ import { z as z22 } from "zod";
1002
+ function registerListCanvases(server2, client2) {
1003
+ server2.registerTool(
1004
+ "naumu_list_canvases",
1005
+ {
1006
+ title: "List Canvases",
1007
+ annotations: { readOnlyHint: true },
1008
+ description: "List freeform drawing canvases in a graph. Each canvas is an Excalidraw-style sketch surface that may contain shapes, text, freehand strokes, images, bookmark cards, and embedded references to graph nodes.",
1009
+ inputSchema: z22.object({
1010
+ graphId: z22.string().describe("The graph ID to list canvases for")
1011
+ })
1012
+ },
1013
+ async ({ graphId }) => {
1014
+ const data = await client2.get(`/api/canvases?graphId=${encodeURIComponent(graphId)}`);
1015
+ return {
1016
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1017
+ };
1018
+ }
1019
+ );
1020
+ }
1021
+
1022
+ // ../mcp-core/src/tools/get-canvas-elements.ts
1023
+ import { z as z23 } from "zod";
1024
+ function registerGetCanvasElements(server2, client2) {
1025
+ server2.registerTool(
1026
+ "naumu_get_canvas_elements",
1027
+ {
1028
+ title: "Get Canvas Elements",
1029
+ annotations: { readOnlyHint: true },
1030
+ description: "Read the structured JSON contents of a canvas \u2014 every shape, text, line, freehand stroke, image, bookmark card, and entity-embed with their positions, colors, and text. Use this when you need to reason about what a user has drawn without needing to see the visual output.",
1031
+ inputSchema: z23.object({
1032
+ canvasId: z23.string().describe("The canvas ID")
1033
+ })
1034
+ },
1035
+ async ({ canvasId }) => {
1036
+ const data = await client2.get(`/api/canvases/${encodeURIComponent(canvasId)}/elements`);
1037
+ return {
1038
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1039
+ };
1040
+ }
1041
+ );
1042
+ }
1043
+
1044
+ // ../mcp-core/src/tools/get-canvas-image.ts
1045
+ import { z as z24 } from "zod";
1046
+ var MAX_INLINE_BYTES = 4 * 1024 * 1024;
1047
+ function registerGetCanvasImage(server2, client2) {
1048
+ server2.registerTool(
1049
+ "naumu_get_canvas_image",
1050
+ {
1051
+ title: "Get Canvas Image",
1052
+ annotations: { readOnlyHint: true },
1053
+ description: 'Render a canvas to a PNG image so you can visually inspect what was drawn \u2014 freehand strokes, spatial layout, sketches. Returns the image inline by default so any MCP host can see it directly. Use `mode: "url"` if you specifically need a signed URL (e.g. very large canvases, or to share the link).',
1054
+ inputSchema: z24.object({
1055
+ canvasId: z24.string().describe("The canvas ID"),
1056
+ scale: z24.union([z24.literal(1), z24.literal(2)]).default(2).describe("Pixel density (1 or 2). Default is 2 for retina-quality output."),
1057
+ theme: z24.enum(["light", "dark"]).default("light").describe("Background theme to render with."),
1058
+ mode: z24.enum(["inline", "url"]).default("inline").describe(
1059
+ "`inline` (default) embeds the PNG directly so vision-capable hosts see it. `url` returns a signed S3 URL that expires in 1 hour \u2014 useful for large canvases or sharing."
1060
+ )
1061
+ })
1062
+ },
1063
+ async ({ canvasId, scale, theme, mode }) => {
1064
+ if (mode === "url") {
1065
+ return await fetchAsUrl(client2, canvasId, scale, theme);
1066
+ }
1067
+ const params = new URLSearchParams({
1068
+ scale: String(scale),
1069
+ theme
1070
+ });
1071
+ const { buffer, contentType } = await client2.getBinary(
1072
+ `/api/canvases/${encodeURIComponent(canvasId)}/image?${params.toString()}`
1073
+ );
1074
+ if (buffer.byteLength > MAX_INLINE_BYTES) {
1075
+ return await fetchAsUrl(client2, canvasId, scale, theme, {
1076
+ reason: `Canvas PNG is ${formatBytes(buffer.byteLength)}, exceeds inline limit ${formatBytes(MAX_INLINE_BYTES)} \u2014 returning signed URL instead.`
1077
+ });
1078
+ }
1079
+ const base64 = bufferToBase64(buffer);
1080
+ return {
1081
+ content: [
1082
+ {
1083
+ type: "image",
1084
+ data: base64,
1085
+ mimeType: contentType.startsWith("image/") ? contentType : "image/png"
1086
+ }
1087
+ ]
1088
+ };
1089
+ }
1090
+ );
1091
+ }
1092
+ async function fetchAsUrl(client2, canvasId, scale, theme, extra) {
1093
+ const params = new URLSearchParams({
1094
+ as: "url",
1095
+ scale: String(scale),
1096
+ theme
1097
+ });
1098
+ const data = await client2.get(
1099
+ `/api/canvases/${encodeURIComponent(canvasId)}/image?${params.toString()}`
1100
+ );
1101
+ const payload = extra ? { ...data, note: extra.reason } : data;
1102
+ return {
1103
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1104
+ };
1105
+ }
1106
+ function bufferToBase64(buffer) {
1107
+ if (typeof Buffer !== "undefined") {
1108
+ return Buffer.from(buffer).toString("base64");
1109
+ }
1110
+ let binary = "";
1111
+ for (let i = 0; i < buffer.byteLength; i++) {
1112
+ binary += String.fromCharCode(buffer[i]);
1113
+ }
1114
+ return btoa(binary);
1115
+ }
1116
+ function formatBytes(bytes) {
1117
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1118
+ return `${(bytes / 1024).toFixed(1)}KB`;
1119
+ }
1120
+
1121
+ // ../mcp-core/src/tools/post-message.ts
1122
+ import { z as z25 } from "zod";
1123
+ function registerPostMessage(server2, client2) {
1124
+ server2.registerTool(
1125
+ "naumu_post_message",
1126
+ {
1127
+ title: "Post Message",
1128
+ annotations: { destructiveHint: true },
1129
+ description: 'Post a message in a Naumu thread you are participating in. Use this to reply to humans (or other bots) in a thread that pinged you. Plain text is accepted by default; for rendered @mentions, pass a Tiptap JSON document with mention nodes (`{ type: "mention", attrs: { id, label } }`) and set contentFormat to "tiptap". To attach files (reports, images, audio, video) call `naumu_request_attachment_upload` first for each file, PUT the bytes to the returned `uploadUrl`, then pass the resulting `attachmentId`s here as `attachmentIds`. The message must have either `content` or `attachmentIds` (or both). Returns the created message JSON.',
1130
+ inputSchema: z25.object({
1131
+ threadId: z25.string().describe("The thread ID to post into. You must be a participant in this thread."),
1132
+ content: z25.string().optional().describe('Message body. Plain text by default; pass a Tiptap JSON document only if contentFormat is set to "tiptap". Optional when `attachmentIds` is provided.'),
1133
+ contentFormat: z25.enum(["tiptap", "text"]).optional().describe('Format of `content`. Defaults to "text". Use "tiptap" for rendered mentions/embeds, e.g. a doc containing `{ type: "mention", attrs: { id: userIdOrIdentityId, label: displayName } }`.'),
1134
+ attachmentIds: z25.array(z25.string().min(1)).max(25).optional().describe("Attachment IDs from prior `naumu_request_attachment_upload` calls. Each must be a successfully-uploaded pending attachment in this graph (1-hour TTL). Up to 25 per message.")
1135
+ })
1136
+ },
1137
+ async ({ threadId, content, contentFormat, attachmentIds }) => {
1138
+ try {
1139
+ const body = {
1140
+ contentFormat: contentFormat ?? "text"
1141
+ };
1142
+ if (content !== void 0) body.content = content;
1143
+ if (attachmentIds && attachmentIds.length > 0) body.attachmentIds = attachmentIds;
1144
+ const data = await client2.post(`/api/threads/${threadId}/messages`, body);
1145
+ return {
1146
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1147
+ };
1148
+ } catch (err) {
1149
+ const message = err instanceof Error ? err.message : String(err);
1150
+ return {
1151
+ content: [{ type: "text", text: `Error: ${message}` }],
1152
+ isError: true
1153
+ };
1154
+ }
1155
+ }
1156
+ );
1157
+ }
1158
+
1159
+ // ../mcp-core/src/tools/read-thread.ts
1160
+ import { z as z26 } from "zod";
1161
+ function registerReadThread(server2, client2) {
1162
+ server2.registerTool(
1163
+ "naumu_read_thread",
1164
+ {
1165
+ title: "Read Thread",
1166
+ annotations: { readOnlyHint: true },
1167
+ description: "Read messages from a Naumu thread. Returns paginated history ordered newest-first. Use `before` (timestamp ms) to page back further into older history. Default page size 50, max 200.",
1168
+ inputSchema: z26.object({
1169
+ threadId: z26.string().describe("The thread ID to read from."),
1170
+ before: z26.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
1171
+ limit: z26.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1172
+ })
1173
+ },
1174
+ async ({ threadId, before, limit }) => {
1175
+ try {
1176
+ const params = new URLSearchParams();
1177
+ if (before !== void 0) params.set("before", String(before));
1178
+ if (limit !== void 0) params.set("limit", String(limit));
1179
+ const qs = params.toString();
1180
+ const path = `/api/threads/${threadId}/messages${qs ? `?${qs}` : ""}`;
1181
+ const data = await client2.get(path);
1182
+ return {
1183
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1184
+ };
1185
+ } catch (err) {
1186
+ const message = err instanceof Error ? err.message : String(err);
1187
+ return {
1188
+ content: [{ type: "text", text: `Error: ${message}` }],
1189
+ isError: true
1190
+ };
1191
+ }
1192
+ }
1193
+ );
1194
+ }
1195
+
1196
+ // ../mcp-core/src/tools/ask-naumu.ts
1197
+ import { z as z27 } from "zod";
1198
+ function registerAskNaumu(server2, client2) {
1199
+ server2.registerTool(
1200
+ "naumu_ask_naumu",
1201
+ {
1202
+ title: "Ask Naumu",
1203
+ // NOT read-only: the backend creates a durable sidechannel thread +
1204
+ // message, may seed a system Identity, and spawns a SpaceAgent task
1205
+ // (and consumes a 5/hour budget). Additive, not destructive.
1206
+ annotations: { destructiveHint: false },
1207
+ description: "Ask the system @Naumu Identity (the canonical graph-writer/curator) a question about your graph. Use for questions where direct graph reads would be inefficient \u2014 Naumu has full read access and synthesizes answers. Limited to 5 calls per hour per Identity. Returns { answer, sources, confidence, durationMs }.",
1208
+ inputSchema: z27.object({
1209
+ graphId: z27.string().describe("The graph ID to ask about."),
1210
+ question: z27.string().max(4e3).describe("The question to ask Naumu. Max 4000 characters.")
1211
+ })
1212
+ },
1213
+ async ({ graphId, question }) => {
1214
+ try {
1215
+ const identityId = process.env.NAUMU_IDENTITY_ID;
1216
+ if (!identityId) {
1217
+ return {
1218
+ content: [
1219
+ {
1220
+ type: "text",
1221
+ text: "Error: NAUMU_IDENTITY_ID env var is required for ask_naumu \u2014 re-run the pairing setup."
1222
+ }
1223
+ ],
1224
+ isError: true
1225
+ };
1226
+ }
1227
+ const data = await client2.post(`/api/identities/${identityId}/ask`, {
1228
+ graphId,
1229
+ question
1230
+ });
1231
+ return {
1232
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1233
+ };
1234
+ } catch (err) {
57
1235
  const message = err instanceof Error ? err.message : String(err);
58
- console.error(`[mcp] failed to fetch tool manifest: ${message}`);
59
- process.exit(1);
1236
+ return {
1237
+ content: [{ type: "text", text: `Error: ${message}` }],
1238
+ isError: true
1239
+ };
1240
+ }
60
1241
  }
1242
+ );
61
1243
  }
62
- else {
63
- registerAllTools(server, client);
64
- console.error(`[mcp] connected to ${apiUrl} as user key (${process.env.NAUMU_IDENTITY_ID ?? 'no identity id set'})`);
1244
+
1245
+ // ../mcp-core/src/tools/whoami.ts
1246
+ import { z as z28 } from "zod";
1247
+ function registerWhoami(server2, client2, allToolNames) {
1248
+ server2.registerTool(
1249
+ "naumu_whoami",
1250
+ {
1251
+ title: "Who Am I",
1252
+ annotations: { readOnlyHint: true },
1253
+ description: 'Return who the calling key is plus the live MCP tool manifest, so you can bootstrap before the first real operation. A bot identity key returns its Identity row (id, graphId, name, instructions, allowedTools). A user API key returns `kind: "user"` with userId, name, and email \u2014 a person spans many graphs, so resolve a specific graph via naumu_list_graphs. No arguments. Always available regardless of the permission grid.',
1254
+ inputSchema: z28.object({})
1255
+ },
1256
+ async () => {
1257
+ try {
1258
+ const data = await client2.get("/api/identities/me/whoami");
1259
+ if (allToolNames && data && typeof data === "object" && data.kind === "user") {
1260
+ data.mcpTools = allToolNames;
1261
+ }
1262
+ return {
1263
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1264
+ };
1265
+ } catch (err) {
1266
+ const message = err instanceof Error ? err.message : String(err);
1267
+ return {
1268
+ content: [{ type: "text", text: `Error: ${message}` }],
1269
+ isError: true
1270
+ };
1271
+ }
1272
+ }
1273
+ );
1274
+ }
1275
+
1276
+ // ../mcp-core/src/tools/list-threads.ts
1277
+ import { z as z29 } from "zod";
1278
+ function registerListThreads(server2, client2) {
1279
+ server2.registerTool(
1280
+ "naumu_list_threads",
1281
+ {
1282
+ title: "List Threads",
1283
+ annotations: { readOnlyHint: true },
1284
+ description: "List threads sorted by last activity (newest first), for self-discovery before deciding which to engage. With a user API key, pass `graphId` to list threads you can see in that space (resolve it via naumu_list_graphs). With a bot identity key, omit `graphId` to list threads in your own graph \u2014 each row carries an `isParticipant` flag (TRUE means you were explicitly invited and your replies fan out via webhook). Page back with `cursor` set to the oldest `lastActivityAt` from the previous page.",
1285
+ inputSchema: z29.object({
1286
+ graphId: z29.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1287
+ cursor: z29.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1288
+ limit: z29.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1289
+ })
1290
+ },
1291
+ async ({ graphId, cursor, limit }) => {
1292
+ try {
1293
+ const params = new URLSearchParams();
1294
+ if (cursor !== void 0) params.set("cursor", String(cursor));
1295
+ if (limit !== void 0) params.set("limit", String(limit));
1296
+ let path;
1297
+ if (graphId) {
1298
+ params.set("graphId", graphId);
1299
+ path = `/api/threads/paginated?${params.toString()}`;
1300
+ } else {
1301
+ const qs = params.toString();
1302
+ path = `/api/identities/me/threads${qs ? `?${qs}` : ""}`;
1303
+ }
1304
+ const data = await client2.get(path);
1305
+ return {
1306
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1307
+ };
1308
+ } catch (err) {
1309
+ const message = err instanceof Error ? err.message : String(err);
1310
+ return {
1311
+ content: [{ type: "text", text: `Error: ${message}` }],
1312
+ isError: true
1313
+ };
1314
+ }
1315
+ }
1316
+ );
1317
+ }
1318
+
1319
+ // ../mcp-core/src/tools/get-thread.ts
1320
+ import { z as z30 } from "zod";
1321
+ function registerGetThread(server2, client2) {
1322
+ server2.registerTool(
1323
+ "naumu_get_thread",
1324
+ {
1325
+ title: "Get Thread",
1326
+ annotations: { readOnlyHint: true },
1327
+ description: "Fetch a single thread, including the human participant roster (`participantDetails` \u2014 userId, name, image) and bot roster (`identityParticipants` \u2014 id, name, isSystem). Use this when `naumu_list_threads` surfaced a candidate and you want to know exactly who is in it before posting. Pair with `naumu_read_thread` for message history.",
1328
+ inputSchema: z30.object({
1329
+ threadId: z30.string().describe("The thread ID to fetch.")
1330
+ })
1331
+ },
1332
+ async ({ threadId }) => {
1333
+ try {
1334
+ const data = await client2.get(`/api/threads/${threadId}`);
1335
+ return {
1336
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1337
+ };
1338
+ } catch (err) {
1339
+ const message = err instanceof Error ? err.message : String(err);
1340
+ return {
1341
+ content: [{ type: "text", text: `Error: ${message}` }],
1342
+ isError: true
1343
+ };
1344
+ }
1345
+ }
1346
+ );
1347
+ }
1348
+
1349
+ // ../mcp-core/src/tools/create-thread.ts
1350
+ import { z as z31 } from "zod";
1351
+ function registerCreateThread(server2, client2) {
1352
+ server2.registerTool(
1353
+ "naumu_create_thread",
1354
+ {
1355
+ title: "Create Thread",
1356
+ annotations: { destructiveHint: true },
1357
+ description: 'Start a new conversation in your graph. You are auto-attached as a permanent participant; the thread\'s formal creator is your primary owner (the user who registered you), so the thread shows up in their sidebar. Optional `participants` adds humans (by userId) and other bots (by identityId) at creation time. Optional `initialMessage` is posted as your first message \u2014 use it to open the conversation. Returns the created thread (including its id) so you can immediately call `naumu_post_message` for follow-ups. Useful for scheduled briefings ("Brief me about X every morning at 9am" \u2014 schedule on your side, then call this to deliver into Naumu).',
1358
+ inputSchema: z31.object({
1359
+ title: z31.string().min(1).max(200).optional().describe('Thread title shown in the sidebar. If omitted, Naumu generates a default like "Conversation YYYY-MM-DD".'),
1360
+ participants: z31.array(
1361
+ z31.discriminatedUnion("type", [
1362
+ z31.object({
1363
+ type: z31.literal("user"),
1364
+ userId: z31.string().min(1).describe("User UUID \u2014 get these from `naumu_get_thread`/`naumu_read_thread` participant rosters or webhook payloads.")
1365
+ }),
1366
+ z31.object({
1367
+ type: z31.literal("identity"),
1368
+ identityId: z31.string().min(1).describe("Identity id (`identity-\u2026` or `id-\u2026`). Other bots in the same graph can be co-attached to multi-bot threads.")
1369
+ })
1370
+ ])
1371
+ ).max(32).optional().describe("Up to 32 humans and/or other bots to attach at creation. Your primary owner is added automatically \u2014 you do NOT need to list them here."),
1372
+ initialMessage: z31.string().min(1).max(32e3).optional().describe("Markdown body for the first message. Authored by you (the bot), so it appears in the thread under your name."),
1373
+ visibility: z31.enum(["restricted", "internal", "open"]).optional().describe("`restricted` (invite-only, default) hides from non-participants. `internal` is visible to space members. `open` is visible to anyone who can see the space.")
1374
+ })
1375
+ },
1376
+ async ({ title, participants, initialMessage, visibility }) => {
1377
+ try {
1378
+ const body = {};
1379
+ if (title !== void 0) body.title = title;
1380
+ if (participants !== void 0) body.participants = participants;
1381
+ if (initialMessage !== void 0) body.initialMessage = initialMessage;
1382
+ if (visibility !== void 0) body.visibility = visibility;
1383
+ const data = await client2.post("/api/identities/me/threads", body);
1384
+ return {
1385
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1386
+ };
1387
+ } catch (err) {
1388
+ const message = err instanceof Error ? err.message : String(err);
1389
+ return {
1390
+ content: [{ type: "text", text: `Error: ${message}` }],
1391
+ isError: true
1392
+ };
1393
+ }
1394
+ }
1395
+ );
1396
+ }
1397
+
1398
+ // ../mcp-core/src/tools/request-attachment-upload.ts
1399
+ import { z as z32 } from "zod";
1400
+ function registerRequestAttachmentUpload(server2, client2) {
1401
+ server2.registerTool(
1402
+ "naumu_request_attachment_upload",
1403
+ {
1404
+ title: "Request Attachment Upload",
1405
+ annotations: { destructiveHint: true },
1406
+ description: 'Request a presigned S3 upload URL to attach a file to a message. Same flow Naumu users use for file uploads: get a signed URL, PUT the bytes to it directly, then call `naumu_post_message` with the returned `attachmentId` in `attachmentIds`. Use this when you want to deliver generated content as a file (a markdown report, a PDF, an image, an audio recording, a video). Per-MIME size caps apply (typically 50MB umbrella, 10MB for agent-readable types).\n\nReturns `{ attachmentId, uploadUrl, method, requiredHeaders, expiresAt }`. Use these EXACTLY:\n\u2022 `method` is "PUT".\n\u2022 Send every header in `requiredHeaders` (Content-Type matters for S3 signature validation).\n\u2022 Do NOT add an Authorization header \u2014 the URL itself is the auth.\n\u2022 Do NOT log `uploadUrl` \u2014 it is a bearer capability for the duration of the TTL.\n\u2022 `expiresAt` is a Unix-ms timestamp; the pending attachment vanishes at that moment whether or not you uploaded. Call `naumu_post_message` with the attachmentId before then or the upload orphans.\n\nServer-side checks at post time enforce that the attachment was uploaded by you, in this graph, for this thread \u2014 you cannot reuse an upload across threads.',
1407
+ inputSchema: z32.object({
1408
+ threadId: z32.string().describe("Thread the attachment will land in. You must be a participant. The pending attachment is keyed to this thread \u2014 you cannot reuse it for a different one."),
1409
+ fileName: z32.string().min(1).describe("Original filename (with extension). Used as the display name in the message and for the S3 object suffix. Special characters are sanitized server-side."),
1410
+ fileType: z32.string().min(1).describe("MIME type, e.g. `application/pdf`, `image/png`, `text/markdown`, `audio/mpeg`, `video/mp4`. The S3 PUT will enforce this Content-Type."),
1411
+ fileSize: z32.number().int().positive().describe("File size in bytes. Validated against per-MIME caps before the URL is issued \u2014 exceeding the cap returns a 400."),
1412
+ audioDurationSec: z32.number().positive().optional().describe("For audio attachments, duration in seconds. Validated against the audio recording cap (currently 8 hours).")
1413
+ })
1414
+ },
1415
+ async ({ threadId, fileName, fileType, fileSize, audioDurationSec }) => {
1416
+ try {
1417
+ const me = await client2.get("/api/identities/me/whoami");
1418
+ const body = {
1419
+ graphId: me.graphId,
1420
+ threadId,
1421
+ fileName,
1422
+ fileType,
1423
+ fileSize
1424
+ };
1425
+ if (audioDurationSec !== void 0) body.audioDurationSec = audioDurationSec;
1426
+ const data = await client2.post("/api/attachments/presign", body);
1427
+ return {
1428
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1429
+ };
1430
+ } catch (err) {
1431
+ const message = err instanceof Error ? err.message : String(err);
1432
+ return {
1433
+ content: [{ type: "text", text: `Error: ${message}` }],
1434
+ isError: true
1435
+ };
1436
+ }
1437
+ }
1438
+ );
1439
+ }
1440
+
1441
+ // ../mcp-core/src/tools/add-reaction.ts
1442
+ import { z as z33 } from "zod";
1443
+ function registerAddReaction(server2, client2) {
1444
+ server2.registerTool(
1445
+ "naumu_add_reaction",
1446
+ {
1447
+ title: "Add Reaction",
1448
+ annotations: { destructiveHint: true },
1449
+ description: 'Add an emoji reaction to a message in a thread you are participating in. Idempotent \u2014 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 \u2014 situations where words are required.\n\u2022 For long tasks: react \u{1F440} first to acknowledge, optionally post a short "On it \u2014 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.',
1450
+ inputSchema: z33.object({
1451
+ threadId: z33.string().describe("Thread containing the message. You must be a participant."),
1452
+ messageId: z33.string().describe("The message to react to."),
1453
+ emoji: z33.string().min(1).describe('Emoji character (e.g. "\u{1F440}", "\u2705", "\u2764\uFE0F"). Custom-emoji shortcodes are NOT supported here \u2014 pass a real Unicode emoji.')
1454
+ })
1455
+ },
1456
+ async ({ threadId, messageId, emoji }) => {
1457
+ try {
1458
+ const data = await client2.post(
1459
+ `/api/threads/${threadId}/messages/${messageId}/reactions`,
1460
+ { emoji, action: "add" }
1461
+ );
1462
+ return {
1463
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1464
+ };
1465
+ } catch (err) {
1466
+ const message = err instanceof Error ? err.message : String(err);
1467
+ return {
1468
+ content: [{ type: "text", text: `Error: ${message}` }],
1469
+ isError: true
1470
+ };
1471
+ }
1472
+ }
1473
+ );
1474
+ }
1475
+
1476
+ // ../mcp-core/src/tools/remove-reaction.ts
1477
+ import { z as z34 } from "zod";
1478
+ function registerRemoveReaction(server2, client2) {
1479
+ server2.registerTool(
1480
+ "naumu_remove_reaction",
1481
+ {
1482
+ title: "Remove Reaction",
1483
+ annotations: { destructiveHint: true },
1484
+ description: "Remove your own emoji reaction from a message. Idempotent \u2014 calling on a reaction you never added is a no-op. Pair with `naumu_add_reaction` when you need to walk back an acknowledgement (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 }` \u2014 `alreadyExisted: false` means there was nothing to remove and the call was a no-op.",
1485
+ inputSchema: z34.object({
1486
+ threadId: z34.string().describe("Thread containing the message. You must be a participant."),
1487
+ messageId: z34.string().describe("The message to remove your reaction from."),
1488
+ emoji: z34.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1489
+ })
1490
+ },
1491
+ async ({ threadId, messageId, emoji }) => {
1492
+ try {
1493
+ const data = await client2.post(
1494
+ `/api/threads/${threadId}/messages/${messageId}/reactions`,
1495
+ { emoji, action: "remove" }
1496
+ );
1497
+ return {
1498
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1499
+ };
1500
+ } catch (err) {
1501
+ const message = err instanceof Error ? err.message : String(err);
1502
+ return {
1503
+ content: [{ type: "text", text: `Error: ${message}` }],
1504
+ isError: true
1505
+ };
1506
+ }
1507
+ }
1508
+ );
1509
+ }
1510
+
1511
+ // ../mcp-core/src/tools/naumu-typing.ts
1512
+ import { z as z35 } from "zod";
1513
+ function registerNaumuTyping(server2, client2) {
1514
+ server2.registerTool(
1515
+ "naumu_typing",
1516
+ {
1517
+ title: "Set Typing Indicator",
1518
+ // Purely ephemeral: drives an in-memory WS typing lease (no durable
1519
+ // state, self-expires, trivially reversible via "stop"). Not destructive.
1520
+ annotations: { destructiveHint: false },
1521
+ description: 'Show or hide your "is typing\u2026" pill in a thread. Call with `state: "start"` the moment you decide to compose a reply (before any LLM call), and the server holds the pill alive \u2014 re-broadcasting on a short interval \u2014 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 \u2014 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.',
1522
+ inputSchema: z35.object({
1523
+ threadId: z35.string().describe("The thread ID to set typing in. You must be a participant."),
1524
+ state: z35.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1525
+ })
1526
+ },
1527
+ async ({ threadId, state }) => {
1528
+ try {
1529
+ const data = await client2.post(`/api/threads/${threadId}/typing`, { state });
1530
+ return {
1531
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1532
+ };
1533
+ } catch (err) {
1534
+ const message = err instanceof Error ? err.message : String(err);
1535
+ return {
1536
+ content: [{ type: "text", text: `Error: ${message}` }],
1537
+ isError: true
1538
+ };
1539
+ }
1540
+ }
1541
+ );
1542
+ }
1543
+
1544
+ // ../mcp-core/src/tools/note-read.ts
1545
+ import { z as z36 } from "zod";
1546
+ function registerNoteRead(server2, client2) {
1547
+ server2.registerTool(
1548
+ "naumu_note_read",
1549
+ {
1550
+ title: "Read Note",
1551
+ annotations: { readOnlyHint: true },
1552
+ description: "Read the current contents of a note as markdown. Use before editing so you know what you're working with \u2014 `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.",
1553
+ inputSchema: z36.object({
1554
+ noteId: z36.string().describe("The note (Thought) ID")
1555
+ })
1556
+ },
1557
+ async ({ noteId }) => {
1558
+ const data = await client2.get(`/api/notes/${noteId}/markdown`);
1559
+ return {
1560
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1561
+ };
1562
+ }
1563
+ );
1564
+ }
1565
+
1566
+ // ../mcp-core/src/tools/note-append.ts
1567
+ import { z as z37 } from "zod";
1568
+ function registerNoteAppend(server2, client2) {
1569
+ server2.registerTool(
1570
+ "naumu_note_append",
1571
+ {
1572
+ title: "Append to Note",
1573
+ annotations: { destructiveHint: true },
1574
+ description: "Append markdown blocks to the end of a note. Other participants see your colored cursor while the write lands. Markdown supports headings (1\u20133), bold/italic/code, lists, blockquotes, code blocks, links, and tables.",
1575
+ inputSchema: z37.object({
1576
+ noteId: z37.string().describe("The note (Thought) ID to append to"),
1577
+ markdown: z37.string().min(1).describe("Markdown content to append at the end of the note")
1578
+ })
1579
+ },
1580
+ async ({ noteId, markdown }) => {
1581
+ const data = await client2.post(`/api/notes/${noteId}/append`, { markdown });
1582
+ return {
1583
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1584
+ };
1585
+ }
1586
+ );
1587
+ }
1588
+
1589
+ // ../mcp-core/src/tools/note-insert.ts
1590
+ import { z as z38 } from "zod";
1591
+ function registerNoteInsert(server2, client2) {
1592
+ server2.registerTool(
1593
+ "naumu_note_insert",
1594
+ {
1595
+ title: "Insert After Heading",
1596
+ annotations: { destructiveHint: true },
1597
+ description: "Insert markdown content into a note immediately after a named section. The section ends at the next heading of equal-or-higher level (or end of doc). 404 if no heading matches `headingText` exactly \u2014 call `naumu_note_read` first to see the live structure.",
1598
+ inputSchema: z38.object({
1599
+ noteId: z38.string().describe("The note (Thought) ID"),
1600
+ headingText: z38.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1601
+ markdown: z38.string().min(1).describe("Markdown content to insert at the end of that section")
1602
+ })
1603
+ },
1604
+ async ({ noteId, headingText, markdown }) => {
1605
+ const data = await client2.post(`/api/notes/${noteId}/insert-after`, {
1606
+ headingText,
1607
+ markdown
1608
+ });
1609
+ return {
1610
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1611
+ };
1612
+ }
1613
+ );
1614
+ }
1615
+
1616
+ // ../mcp-core/src/tools/note-replace-section.ts
1617
+ import { z as z39 } from "zod";
1618
+ function registerNoteReplaceSection(server2, client2) {
1619
+ server2.registerTool(
1620
+ "naumu_note_replace_section",
1621
+ {
1622
+ title: "Replace Section",
1623
+ annotations: { destructiveHint: true },
1624
+ description: "Replace the body under a named heading with new markdown. By default the heading row itself is preserved (set `keepHeading: false` to drop it too). 404 if no heading matches.",
1625
+ inputSchema: z39.object({
1626
+ noteId: z39.string().describe("The note (Thought) ID"),
1627
+ headingText: z39.string().min(1).describe("Exact text of the heading anchoring the section"),
1628
+ markdown: z39.string().describe("Replacement markdown for the section body"),
1629
+ keepHeading: z39.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1630
+ })
1631
+ },
1632
+ async ({ noteId, headingText, markdown, keepHeading }) => {
1633
+ const data = await client2.post(`/api/notes/${noteId}/replace-section`, {
1634
+ headingText,
1635
+ markdown,
1636
+ keepHeading
1637
+ });
1638
+ return {
1639
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1640
+ };
1641
+ }
1642
+ );
1643
+ }
1644
+
1645
+ // ../mcp-core/src/tools/note-delete-section.ts
1646
+ import { z as z40 } from "zod";
1647
+ function registerNoteDeleteSection(server2, client2) {
1648
+ server2.registerTool(
1649
+ "naumu_note_delete_section",
1650
+ {
1651
+ title: "Delete Section",
1652
+ annotations: { destructiveHint: true },
1653
+ description: "\u26A0 DESTRUCTIVE: removes a heading row plus its body (down to the next heading of equal-or-higher level). Anything inside that section is gone \u2014 there is no per-call undo. ONLY use when the user explicitly asks to drop a section. 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.",
1654
+ inputSchema: z40.object({
1655
+ noteId: z40.string().describe("The note (Thought) ID"),
1656
+ headingText: z40.string().min(1).describe("Exact text of the heading whose section will be deleted")
1657
+ })
1658
+ },
1659
+ async ({ noteId, headingText }) => {
1660
+ const data = await client2.post(`/api/notes/${noteId}/delete-section`, {
1661
+ headingText
1662
+ });
1663
+ return {
1664
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1665
+ };
1666
+ }
1667
+ );
1668
+ }
1669
+
1670
+ // ../mcp-core/src/tools/note-replace.ts
1671
+ import { z as z41 } from "zod";
1672
+ function registerNoteReplace(server2, client2) {
1673
+ server2.registerTool(
1674
+ "naumu_note_replace",
1675
+ {
1676
+ title: "Replace Note",
1677
+ annotations: { destructiveHint: true },
1678
+ description: "\u26A0 DESTRUCTIVE: replaces the entire note content with new markdown. Any concurrent human edits made during the call are silently overwritten. ONLY use when the user explicitly asks to rewrite/replace the whole note. 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.",
1679
+ inputSchema: z41.object({
1680
+ noteId: z41.string().describe("The note (Thought) ID"),
1681
+ markdown: z41.string().describe("New markdown content for the entire note")
1682
+ })
1683
+ },
1684
+ async ({ noteId, markdown }) => {
1685
+ const data = await client2.post(`/api/notes/${noteId}/replace`, { markdown });
1686
+ return {
1687
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1688
+ };
1689
+ }
1690
+ );
1691
+ }
1692
+
1693
+ // ../mcp-core/src/tools/note-find-replace.ts
1694
+ import { z as z42 } from "zod";
1695
+ function registerNoteFindReplace(server2, client2) {
1696
+ server2.registerTool(
1697
+ "naumu_note_find_replace",
1698
+ {
1699
+ title: "Find/Replace in Note",
1700
+ annotations: { destructiveHint: true },
1701
+ description: "Literal find/replace within a note's text content. Marks (bold, italic, code, etc.) are preserved on the surrounding text. Use this for mid-paragraph tweaks \u2014 the section-based tools can't target inline substrings. \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.",
1702
+ inputSchema: z42.object({
1703
+ noteId: z42.string().describe("The note (Thought) ID"),
1704
+ find: z42.string().min(1).describe("Substring to search for. Literal \u2014 no regex."),
1705
+ replace: z42.string().describe("Replacement string. May be empty to delete the match."),
1706
+ all: z42.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1707
+ })
1708
+ },
1709
+ async ({ noteId, find, replace, all }) => {
1710
+ const data = await client2.post(`/api/notes/${noteId}/find-replace`, {
1711
+ find,
1712
+ replace,
1713
+ all
1714
+ });
1715
+ return {
1716
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1717
+ };
1718
+ }
1719
+ );
1720
+ }
1721
+
1722
+ // ../mcp-core/src/tools/canvas-add-element.ts
1723
+ import { z as z43 } from "zod";
1724
+ var ELEMENT_TYPES = [
1725
+ "rectangle",
1726
+ "ellipse",
1727
+ "diamond",
1728
+ "text",
1729
+ "line",
1730
+ "arrow",
1731
+ "freehand",
1732
+ "image",
1733
+ "bookmark-card",
1734
+ "entity-embed"
1735
+ ];
1736
+ function registerCanvasAddElement(server2, client2) {
1737
+ server2.registerTool(
1738
+ "naumu_canvas_add_element",
1739
+ {
1740
+ title: "Add Canvas Element",
1741
+ annotations: { destructiveHint: true },
1742
+ description: "Add a new element to a canvas. Other participants see your colored cursor at the element's center while it lands. The server fills in `id`, `version`, `fractionalIndex`, and `seed` automatically \u2014 pass only `type`, position (`x`, `y`), and size (`width`, `height`); other fields are optional and have sensible defaults (stroke #1e1e1e, no fill, opacity 1, etc.). For text elements, set `label` and `fontSize`. Coordinates are canvas-space (the same units the live editor uses).",
1743
+ inputSchema: z43.object({
1744
+ canvasId: z43.string().describe("The canvas ID"),
1745
+ element: z43.object({
1746
+ type: z43.enum(ELEMENT_TYPES).describe("Element shape"),
1747
+ x: z43.number().describe("Top-left x coordinate in canvas space"),
1748
+ y: z43.number().describe("Top-left y coordinate in canvas space"),
1749
+ width: z43.number().describe("Width in canvas units"),
1750
+ height: z43.number().describe("Height in canvas units"),
1751
+ strokeColor: z43.string().optional().describe("Stroke color (hex). Default #1e1e1e."),
1752
+ fillColor: z43.string().optional().describe('Fill color (hex) or "transparent". Default transparent.'),
1753
+ strokeWidth: z43.number().optional().describe("Stroke width. Default 2."),
1754
+ opacity: z43.number().min(0).max(1).optional().describe("0..1, default 1"),
1755
+ roughness: z43.number().min(0).max(2).optional().describe("Hand-drawn roughness 0..2. Default 1."),
1756
+ label: z43.string().optional().describe("Optional label/text content"),
1757
+ labelFontSize: z43.number().optional().describe("Label font size"),
1758
+ angle: z43.number().optional().describe("Rotation in radians. Default 0.")
1759
+ }).passthrough().describe("Element fields. Pass only what you set \u2014 defaults fill the rest.")
1760
+ })
1761
+ },
1762
+ async ({ canvasId, element }) => {
1763
+ const data = await client2.post(`/api/canvases/${canvasId}/elements`, { element });
1764
+ return {
1765
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1766
+ };
1767
+ }
1768
+ );
1769
+ }
1770
+
1771
+ // ../mcp-core/src/tools/canvas-update-element.ts
1772
+ import { z as z44 } from "zod";
1773
+ function registerCanvasUpdateElement(server2, client2) {
1774
+ server2.registerTool(
1775
+ "naumu_canvas_update_element",
1776
+ {
1777
+ title: "Update Canvas Element",
1778
+ annotations: { destructiveHint: true },
1779
+ description: "Patch an existing canvas element. Only the fields you pass in `changes` are updated \u2014 `id`, `version`, `createdBy`, and `isDeleted` are server-managed and ignored if present. Useful for moving (`x`, `y`), resizing (`width`, `height`), recoloring (`strokeColor`, `fillColor`), or relabeling (`label`).",
1780
+ inputSchema: z44.object({
1781
+ canvasId: z44.string().describe("The canvas ID"),
1782
+ elementId: z44.string().describe("The element ID returned by `naumu_canvas_add_element` or `naumu_get_canvas_elements`"),
1783
+ changes: z44.record(z44.string(), z44.unknown()).describe("Partial element fields to merge in. Server bumps `version` automatically.")
1784
+ })
1785
+ },
1786
+ async ({ canvasId, elementId, changes }) => {
1787
+ const data = await client2.patch(
1788
+ `/api/canvases/${canvasId}/elements/${elementId}`,
1789
+ { changes }
1790
+ );
1791
+ return {
1792
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1793
+ };
1794
+ }
1795
+ );
1796
+ }
1797
+
1798
+ // ../mcp-core/src/tools/canvas-remove-element.ts
1799
+ import { z as z45 } from "zod";
1800
+ function registerCanvasRemoveElement(server2, client2) {
1801
+ server2.registerTool(
1802
+ "naumu_canvas_remove_element",
1803
+ {
1804
+ title: "Remove Canvas Element",
1805
+ annotations: { destructiveHint: true },
1806
+ description: "Soft-delete a canvas element. The element is tombstoned (isDeleted=true) so concurrent edits don't resurrect it. 404 if the element id is not present on this canvas.",
1807
+ inputSchema: z45.object({
1808
+ canvasId: z45.string().describe("The canvas ID"),
1809
+ elementId: z45.string().describe("The element ID to delete")
1810
+ })
1811
+ },
1812
+ async ({ canvasId, elementId }) => {
1813
+ const data = await client2.del(`/api/canvases/${canvasId}/elements/${elementId}`);
1814
+ return {
1815
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1816
+ };
1817
+ }
1818
+ );
1819
+ }
1820
+
1821
+ // ../mcp-core/src/tools/create-note.ts
1822
+ import { z as z46 } from "zod";
1823
+ function registerCreateNote(server2, client2) {
1824
+ server2.registerTool(
1825
+ "naumu_create_note",
1826
+ {
1827
+ title: "Create Note",
1828
+ annotations: { destructiveHint: true },
1829
+ description: "Create a new empty note in a graph. Returns the new note row including its `id` \u2014 pass that id to `naumu_note_append` / `naumu_note_replace` to fill in the content. Bots can only create notes in their own graph.",
1830
+ inputSchema: z46.object({
1831
+ graphId: z46.string().describe("The graph ID to create the note in"),
1832
+ title: z46.string().optional().describe("Optional title for the note")
1833
+ })
1834
+ },
1835
+ async ({ graphId, title }) => {
1836
+ const data = await client2.post("/api/notes", { graphId, title });
1837
+ return {
1838
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1839
+ };
1840
+ }
1841
+ );
1842
+ }
1843
+
1844
+ // ../mcp-core/src/tools/create-canvas.ts
1845
+ import { z as z47 } from "zod";
1846
+ function registerCreateCanvas(server2, client2) {
1847
+ server2.registerTool(
1848
+ "naumu_create_canvas",
1849
+ {
1850
+ title: "Create Canvas",
1851
+ annotations: { destructiveHint: true },
1852
+ description: "Create a new empty canvas in a graph. Returns the new canvas row including its `id` \u2014 pass that id to `naumu_canvas_add_element` to start drawing. Bots can only create canvases in their own graph.",
1853
+ inputSchema: z47.object({
1854
+ graphId: z47.string().describe("The graph ID to create the canvas in"),
1855
+ title: z47.string().optional().describe("Optional title for the canvas")
1856
+ })
1857
+ },
1858
+ async ({ graphId, title }) => {
1859
+ const data = await client2.post("/api/canvases", { graphId, title });
1860
+ return {
1861
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1862
+ };
1863
+ }
1864
+ );
1865
+ }
1866
+
1867
+ // ../mcp-core/src/tools/list-schema-violations.ts
1868
+ import { z as z48 } from "zod";
1869
+ function registerListSchemaViolations(server2, client2) {
1870
+ server2.registerTool(
1871
+ "naumu_list_schema_violations",
1872
+ {
1873
+ title: "List Schema Violations",
1874
+ annotations: { readOnlyHint: true },
1875
+ description: "List every schema-validation issue in a graph. Returns each node with issues and a summary by violation kind: parent_missing (schema expects a parent edge that does not exist), parent_multiple (more than one parent edge on a node that should have one), parent_mismatch (parent edge has wrong target type or relation label), unknown_relation (edge uses a relation label not defined in the schema), invalid_connection_target (edge connects to a type the schema does not allow for this source). Use for audits, import-verification, and CI-style checks after batch writes.",
1876
+ inputSchema: z48.object({
1877
+ graphId: z48.string().describe("The graph ID")
1878
+ })
1879
+ },
1880
+ async ({ graphId }) => {
1881
+ const data = await client2.get(
1882
+ `/api/graphs/${graphId}/schema/validation`
1883
+ );
1884
+ return {
1885
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1886
+ };
1887
+ }
1888
+ );
1889
+ }
1890
+
1891
+ // ../mcp-core/src/tools/list-dense-nodes.ts
1892
+ import { z as z49 } from "zod";
1893
+ function registerListDenseNodes(server2, client2) {
1894
+ server2.registerTool(
1895
+ "naumu_list_dense_nodes",
1896
+ {
1897
+ title: "List Dense Nodes",
1898
+ annotations: { readOnlyHint: true },
1899
+ description: 'Return nodes whose total edge count (in+out, non-system) is \u2265 minConnections, grouped by type. Built for /restructure hub detection: each row includes `same_typed_child_count` \u2014 the number of children of the SAME type as the node (the Naumu hub-pattern signal). 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. ["Feature","Company"]). Cheap to call \u2014 runs a single Cypher aggregation.',
1900
+ inputSchema: z49.object({
1901
+ graphId: z49.string().describe("The graph ID"),
1902
+ minConnections: z49.number().int().min(1).describe("Minimum total edge count (in + out, excluding system relations). Typical: 10 for hub detection, 11 to count only hubs that exceed the round-4 \u226410 threshold."),
1903
+ nodeTypes: z49.array(z49.string()).optional().describe("Optional list of node types to restrict the scan to.")
1904
+ })
1905
+ },
1906
+ async ({ graphId, minConnections, nodeTypes }) => {
1907
+ const params = new URLSearchParams();
1908
+ params.set("minConnections", String(minConnections));
1909
+ if (nodeTypes?.length) params.set("nodeTypes", nodeTypes.join(","));
1910
+ const data = await client2.get(
1911
+ `/api/graphs/${graphId}/dense-nodes?${params.toString()}`
1912
+ );
1913
+ return {
1914
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1915
+ };
1916
+ }
1917
+ );
1918
+ }
1919
+
1920
+ // ../mcp-core/src/tools/list-node-connections.ts
1921
+ import { z as z50 } from "zod";
1922
+ function registerListNodeConnections(server2, client2) {
1923
+ server2.registerTool(
1924
+ "naumu_list_node_connections",
1925
+ {
1926
+ title: "List Node Connections",
1927
+ annotations: { readOnlyHint: true },
1928
+ description: 'Return a single node\'s edges (non-system) with the connected node on the other side. Used 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}}] }`.',
1929
+ inputSchema: z50.object({
1930
+ graphId: z50.string().describe("The graph ID"),
1931
+ nodeId: z50.string().describe("The node ID to inspect"),
1932
+ edgeType: z50.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
1933
+ direction: z50.enum(["in", "out", "both"]).optional().describe('Edge direction filter \u2014 "in" (incoming), "out" (outgoing), "both" (default).')
1934
+ })
1935
+ },
1936
+ async ({ graphId, nodeId, edgeType, direction }) => {
1937
+ const params = new URLSearchParams();
1938
+ if (edgeType) params.set("edgeType", edgeType);
1939
+ if (direction) params.set("direction", direction);
1940
+ const qs = params.toString();
1941
+ const path = `/api/graphs/${graphId}/nodes/${nodeId}/connections${qs ? `?${qs}` : ""}`;
1942
+ const data = await client2.get(path);
1943
+ return {
1944
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1945
+ };
1946
+ }
1947
+ );
1948
+ }
1949
+
1950
+ // ../mcp-core/src/tools/reparent.ts
1951
+ import { z as z51 } from "zod";
1952
+ function registerReparent(server2, client2) {
1953
+ server2.registerTool(
1954
+ "naumu_reparent",
1955
+ {
1956
+ title: "Reparent Node",
1957
+ annotations: { destructiveHint: true },
1958
+ description: 'Atomically swap a node\'s parent edge. 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** \u2014 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"}`. Use during /restructure to reparent children under newly-created mini-hubs.',
1959
+ inputSchema: z51.object({
1960
+ graphId: z51.string().describe("The graph ID"),
1961
+ nodeId: z51.string().describe("The child node to reparent"),
1962
+ newParentId: z51.string().describe("The new parent node id"),
1963
+ newRelation: z51.string().describe('The new parent edge relation label (e.g. "PART_OF"). Must be valid per the schema for (child.type, relation, parent.type).')
1964
+ })
1965
+ },
1966
+ async ({ graphId, nodeId, newParentId, newRelation }) => {
1967
+ const data = await client2.post(
1968
+ `/api/graphs/${graphId}/nodes/${nodeId}/reparent`,
1969
+ { newParentId, newRelation }
1970
+ );
1971
+ return {
1972
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1973
+ };
1974
+ }
1975
+ );
1976
+ }
1977
+
1978
+ // ../mcp-core/src/tools/batch-reparent.ts
1979
+ import { z as z52 } from "zod";
1980
+ function registerBatchReparent(server2, client2) {
1981
+ server2.registerTool(
1982
+ "naumu_batch_reparent",
1983
+ {
1984
+ title: "Batch Reparent Nodes",
1985
+ annotations: { destructiveHint: true },
1986
+ description: 'Reparent 1\u201325 nodes onto a shared `newParentId` with the same `newRelation`. 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?}]`. Used to move a same-typed cluster under a freshly-created mini-hub in /restructure.',
1987
+ inputSchema: z52.object({
1988
+ graphId: z52.string().describe("The graph ID"),
1989
+ newParentId: z52.string().describe("Parent node id every nodeId in the batch will be parented to"),
1990
+ newRelation: z52.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
1991
+ nodeIds: z52.array(z52.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
1992
+ })
1993
+ },
1994
+ async ({ graphId, newParentId, newRelation, nodeIds }) => {
1995
+ const data = await client2.post(`/api/graphs/${graphId}/reparent/bulk`, {
1996
+ newParentId,
1997
+ newRelation,
1998
+ nodeIds
1999
+ });
2000
+ return {
2001
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
2002
+ };
2003
+ }
2004
+ );
2005
+ }
2006
+
2007
+ // ../mcp-core/src/tools/chatgpt-search.ts
2008
+ import { z as z53 } from "zod";
2009
+
2010
+ // ../mcp-core/src/public-origin.ts
2011
+ function publicOrigin() {
2012
+ const raw = process.env.NEXT_PUBLIC_URL || "http://localhost:3000";
2013
+ return raw.replace(/\/+$/, "");
2014
+ }
2015
+
2016
+ // ../mcp-core/src/tools/chatgpt-search.ts
2017
+ var PER_GRAPH_LIMIT = 10;
2018
+ var TOTAL_LIMIT = 20;
2019
+ function encodeResourceId(graphId, nodeId) {
2020
+ return `${graphId}:${nodeId}`;
2021
+ }
2022
+ function mergeSearchResults(scoped, graphById, origin, totalLimit = TOTAL_LIMIT) {
2023
+ const byGraph = /* @__PURE__ */ new Map();
2024
+ for (const { graphId, hit } of scoped) {
2025
+ const list = byGraph.get(graphId);
2026
+ if (list) list.push(hit);
2027
+ else byGraph.set(graphId, [hit]);
2028
+ }
2029
+ for (const hits of byGraph.values()) {
2030
+ hits.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
2031
+ }
2032
+ const queues = Array.from(byGraph.entries());
2033
+ const ordered = [];
2034
+ for (let rank = 0; ordered.length < totalLimit; rank++) {
2035
+ let advanced = false;
2036
+ for (const [graphId, hits] of queues) {
2037
+ if (rank >= hits.length) continue;
2038
+ ordered.push({ graphId, hit: hits[rank] });
2039
+ advanced = true;
2040
+ if (ordered.length >= totalLimit) break;
2041
+ }
2042
+ if (!advanced) break;
2043
+ }
2044
+ return ordered.map(({ graphId, hit }) => {
2045
+ const graph = graphById.get(graphId);
2046
+ const slug = graph?.slug ?? graphId;
2047
+ const label = hit.label || hit.id;
2048
+ return {
2049
+ id: encodeResourceId(graphId, hit.id),
2050
+ title: graph?.name ? `${label} \u2014 ${graph.name}` : label,
2051
+ url: `${origin}/spaces/${slug}/nodes/${hit.id}`
2052
+ };
2053
+ });
2054
+ }
2055
+ function registerChatgptSearch(server2, client2) {
2056
+ server2.registerTool(
2057
+ "search",
2058
+ {
2059
+ title: "Search",
2060
+ annotations: { readOnlyHint: true },
2061
+ 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.)",
2062
+ inputSchema: z53.object({
2063
+ query: z53.string().describe('Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa").')
2064
+ })
2065
+ },
2066
+ async ({ query }) => {
2067
+ const origin = publicOrigin();
2068
+ const graphs = await client2.get("/api/graphs") ?? [];
2069
+ const graphById = /* @__PURE__ */ new Map();
2070
+ for (const g of graphs) graphById.set(g.id, g);
2071
+ const params = new URLSearchParams();
2072
+ params.set("q", query);
2073
+ params.set("limit", String(PER_GRAPH_LIMIT));
2074
+ const perGraph = await Promise.all(
2075
+ graphs.map(async (g) => {
2076
+ try {
2077
+ const hits = await client2.get(
2078
+ `/api/graphs/${g.id}/search/hybrid?${params.toString()}`
2079
+ );
2080
+ return (hits ?? []).map((h) => ({ graphId: g.id, hit: h }));
2081
+ } catch {
2082
+ return [];
2083
+ }
2084
+ })
2085
+ );
2086
+ const results = mergeSearchResults(perGraph.flat(), graphById, origin);
2087
+ return {
2088
+ content: [{ type: "text", text: JSON.stringify({ results }) }]
2089
+ };
2090
+ }
2091
+ );
2092
+ }
2093
+
2094
+ // ../mcp-core/src/tools/chatgpt-fetch.ts
2095
+ import { z as z54 } from "zod";
2096
+ var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2097
+ "id",
2098
+ "label",
2099
+ "title",
2100
+ "name",
2101
+ "type",
2102
+ "content",
2103
+ "connections",
2104
+ "embedding",
2105
+ "sortKey",
2106
+ "createdByUserId",
2107
+ "editableBy",
2108
+ "visibility",
2109
+ "defaultVisibility",
2110
+ "createdAt",
2111
+ "updatedAt",
2112
+ "heatConvSignal",
2113
+ "heatConvComputedAt",
2114
+ "heatComputedAt"
2115
+ ]);
2116
+ function parseResourceId(id) {
2117
+ const sep = id.indexOf(":");
2118
+ if (sep <= 0 || sep === id.length - 1) return null;
2119
+ return { graphId: id.slice(0, sep), nodeId: id.slice(sep + 1) };
2120
+ }
2121
+ function pickTitle(node, fallback) {
2122
+ return node.label || node.title || node.name || fallback;
2123
+ }
2124
+ function formatScalar(value) {
2125
+ if (value === null || value === void 0 || value === "") return null;
2126
+ if (typeof value === "object") return JSON.stringify(value);
2127
+ return String(value);
2128
+ }
2129
+ function buildNodeText(node) {
2130
+ const parts = [];
2131
+ if (node.type) parts.push(`Type: ${String(node.type)}`);
2132
+ if (typeof node.content === "string" && node.content.trim()) parts.push(node.content.trim());
2133
+ const attrLines = Object.entries(node).filter(([key]) => !NON_ATTRIBUTE_PROPS.has(key)).map(([key, value]) => {
2134
+ const scalar = formatScalar(value);
2135
+ return scalar === null ? null : `- ${key}: ${scalar}`;
2136
+ }).filter((line) => line !== null);
2137
+ if (attrLines.length) parts.push(`Attributes:
2138
+ ${attrLines.join("\n")}`);
2139
+ const connections = node.connections;
2140
+ const edgeLines = [];
2141
+ for (const e of connections?.outgoing ?? []) {
2142
+ if (!e?.target) continue;
2143
+ edgeLines.push(`- ${e.label ?? "related"} \u2192 ${e.target}${e.isParent ? " (parent)" : ""}`);
2144
+ }
2145
+ for (const e of connections?.incoming ?? []) {
2146
+ if (!e?.source) continue;
2147
+ edgeLines.push(`- ${e.source} \u2192 ${e.label ?? "related"} (incoming)`);
2148
+ }
2149
+ if (edgeLines.length) parts.push(`Connections:
2150
+ ${edgeLines.join("\n")}`);
2151
+ return parts.join("\n\n");
2152
+ }
2153
+ function registerChatgptFetch(server2, client2) {
2154
+ server2.registerTool(
2155
+ "fetch",
2156
+ {
2157
+ title: "Fetch",
2158
+ annotations: { readOnlyHint: true },
2159
+ 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.",
2160
+ inputSchema: z54.object({
2161
+ id: z54.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2162
+ })
2163
+ },
2164
+ async ({ id }) => {
2165
+ const parsed = parseResourceId(id);
2166
+ if (!parsed) {
2167
+ return {
2168
+ content: [
2169
+ {
2170
+ type: "text",
2171
+ text: `Error: id "${id}" is not in the expected "<graphId>:<nodeId>" form. Use an id returned by the search tool.`
2172
+ }
2173
+ ],
2174
+ isError: true
2175
+ };
2176
+ }
2177
+ const { graphId, nodeId } = parsed;
2178
+ const origin = publicOrigin();
2179
+ const node = await client2.get(`/api/graphs/${graphId}/nodes/${nodeId}`);
2180
+ let slug = graphId;
2181
+ try {
2182
+ const meta = await client2.get(`/api/graphs/resolve/${graphId}`);
2183
+ if (meta?.slug) slug = meta.slug;
2184
+ } catch {
2185
+ }
2186
+ return {
2187
+ content: [
2188
+ {
2189
+ type: "text",
2190
+ text: JSON.stringify({
2191
+ id,
2192
+ title: pickTitle(node, nodeId),
2193
+ text: buildNodeText(node),
2194
+ url: `${origin}/spaces/${slug}/nodes/${nodeId}`
2195
+ })
2196
+ }
2197
+ ]
2198
+ };
2199
+ }
2200
+ );
2201
+ }
2202
+
2203
+ // ../mcp-core/src/tools/index.ts
2204
+ var TOOL_REGISTRARS = {
2205
+ naumu_list_graphs: registerListGraphs,
2206
+ naumu_create_graph: registerCreateGraph,
2207
+ naumu_get_schema: registerGetSchema,
2208
+ naumu_update_schema: registerUpdateSchema,
2209
+ naumu_add_node_type: registerAddNodeType,
2210
+ naumu_add_connection: registerAddConnection,
2211
+ naumu_add_attribute: registerAddAttribute,
2212
+ naumu_search: registerSearch,
2213
+ naumu_filter: registerFilter,
2214
+ naumu_get_node: registerGetNode,
2215
+ naumu_get_view: registerGetView,
2216
+ naumu_list_view_nodes: registerListViewNodes,
2217
+ naumu_add_node: registerAddNode,
2218
+ naumu_update_node: registerUpdateNode,
2219
+ naumu_add_edge: registerAddEdge,
2220
+ naumu_remove_node: registerRemoveNode,
2221
+ naumu_remove_edge: registerRemoveEdge,
2222
+ naumu_remove_edges_bulk: registerRemoveEdgesBulk,
2223
+ naumu_ask: registerAsk,
2224
+ naumu_get_ai_thread: registerGetAiThread,
2225
+ naumu_traverse: registerTraverse,
2226
+ naumu_list_canvases: registerListCanvases,
2227
+ naumu_get_canvas_elements: registerGetCanvasElements,
2228
+ naumu_get_canvas_image: registerGetCanvasImage,
2229
+ naumu_post_message: registerPostMessage,
2230
+ naumu_read_thread: registerReadThread,
2231
+ naumu_ask_naumu: registerAskNaumu,
2232
+ naumu_whoami: registerWhoami,
2233
+ naumu_list_threads: registerListThreads,
2234
+ naumu_get_thread: registerGetThread,
2235
+ naumu_create_thread: registerCreateThread,
2236
+ naumu_request_attachment_upload: registerRequestAttachmentUpload,
2237
+ naumu_add_reaction: registerAddReaction,
2238
+ naumu_remove_reaction: registerRemoveReaction,
2239
+ naumu_typing: registerNaumuTyping,
2240
+ naumu_note_read: registerNoteRead,
2241
+ naumu_note_append: registerNoteAppend,
2242
+ naumu_note_insert: registerNoteInsert,
2243
+ naumu_note_replace_section: registerNoteReplaceSection,
2244
+ naumu_note_delete_section: registerNoteDeleteSection,
2245
+ naumu_note_replace: registerNoteReplace,
2246
+ naumu_note_find_replace: registerNoteFindReplace,
2247
+ naumu_canvas_add_element: registerCanvasAddElement,
2248
+ naumu_canvas_update_element: registerCanvasUpdateElement,
2249
+ naumu_canvas_remove_element: registerCanvasRemoveElement,
2250
+ naumu_create_note: registerCreateNote,
2251
+ naumu_create_canvas: registerCreateCanvas,
2252
+ naumu_list_schema_violations: registerListSchemaViolations,
2253
+ naumu_list_dense_nodes: registerListDenseNodes,
2254
+ naumu_list_node_connections: registerListNodeConnections,
2255
+ naumu_reparent: registerReparent,
2256
+ naumu_batch_reparent: registerBatchReparent,
2257
+ // ChatGPT / Deep Research compatibility: the OpenAI MCP surface requires
2258
+ // tools literally named `search` and `fetch`. These wrap the cross-graph
2259
+ // search + node read and emit the `{ results }` / `{ id, title, text, url }`
2260
+ // shapes that client expects (see chatgpt-search.ts / chatgpt-fetch.ts).
2261
+ search: registerChatgptSearch,
2262
+ fetch: registerChatgptFetch
2263
+ };
2264
+ var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS);
2265
+ function registerAllTools(server2, client2) {
2266
+ for (const [name, registrar] of Object.entries(TOOL_REGISTRARS)) {
2267
+ if (name === "naumu_whoami") {
2268
+ registerWhoami(server2, client2, ALL_TOOL_NAMES);
2269
+ } else {
2270
+ registrar(server2, client2);
2271
+ }
2272
+ }
2273
+ }
2274
+ function registerNamedTools(server2, client2, toolNames) {
2275
+ const registered = [];
2276
+ const skipped = [];
2277
+ for (const name of toolNames) {
2278
+ const registrar = TOOL_REGISTRARS[name];
2279
+ if (registrar) {
2280
+ registrar(server2, client2);
2281
+ registered.push(name);
2282
+ } else {
2283
+ skipped.push(name);
2284
+ }
2285
+ }
2286
+ if (skipped.length) {
2287
+ console.error(
2288
+ `[mcp] skipped ${skipped.length} tool(s) absent from this build: ${skipped.join(", ")}`
2289
+ );
2290
+ }
2291
+ console.error(`[mcp] registered ${registered.length} tool(s) for bot key`);
2292
+ }
2293
+
2294
+ // src/index.ts
2295
+ var DEFAULT_API_URL = "https://naumu.ai";
2296
+ var BOT_KEY_PREFIX = "nmu_bot_";
2297
+ var apiUrl = (process.env.NAUMU_API_URL || DEFAULT_API_URL).replace(/\/api\/?$/, "");
2298
+ var apiKey = process.env.NAUMU_API_KEY;
2299
+ if (!apiKey) {
2300
+ console.error("Missing required environment variable: NAUMU_API_KEY");
2301
+ process.exit(1);
2302
+ }
2303
+ var client = new NaumuClient(apiUrl, apiKey);
2304
+ var server = new McpServer(
2305
+ {
2306
+ name: "naumu",
2307
+ version: "0.4.0"
2308
+ },
2309
+ {
2310
+ instructions: NAUMU_INSTRUCTIONS
2311
+ }
2312
+ );
2313
+ var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX);
2314
+ if (isBotKey) {
2315
+ try {
2316
+ const manifest = await client.get("/api/identities/me/mcp-tool-manifest");
2317
+ registerNamedTools(server, client, manifest.mcpTools);
2318
+ console.error(
2319
+ `[mcp] connected to ${apiUrl} as bot identity ${manifest.identityId} (${manifest.intelligenceKind}); ${manifest.mcpTools.length} tool(s) registered`
2320
+ );
2321
+ } catch (err) {
2322
+ const message = err instanceof Error ? err.message : String(err);
2323
+ console.error(`[mcp] failed to fetch tool manifest: ${message}`);
2324
+ process.exit(1);
2325
+ }
2326
+ } else {
2327
+ registerAllTools(server, client);
2328
+ console.error(
2329
+ `[mcp] connected to ${apiUrl} as user key (${process.env.NAUMU_IDENTITY_ID ?? "no identity id set"})`
2330
+ );
65
2331
  }
66
- const transport = new StdioServerTransport();
2332
+ var transport = new StdioServerTransport();
67
2333
  await server.connect(transport);
68
- //# sourceMappingURL=index.js.map