@naumu/mcp 0.6.2 → 0.6.4

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 (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +483 -419
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -118,6 +118,8 @@ var NaumuClient = class {
118
118
  // ../mcp-core/src/instructions.ts
119
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.
120
120
 
121
+ Getting information about a space: start with naumu_ask. It puts your question to the @Naumu agent (which has full read access) and returns a synthesised answer with the source node ids and a confidence hint, in a single call. It is the go-to for what is in a space, what is new or recently changed, how something works, or any summary. Use the granular read tools when you need precision naumu_ask cannot give: naumu_search (find nodes by meaning), naumu_filter (every node of a type or attribute, deterministic and complete), naumu_list_threads + naumu_read_thread (conversation history). To hand @Naumu work to carry out in the background (add knowledge, make changes, record a status update), use naumu_delegate.
122
+
121
123
  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.
122
124
 
123
125
  How to resolve a slug \u2192 graphId:
@@ -127,7 +129,7 @@ URL \u2192 tool mapping (the value after /spaces/ is the slug \u2014 resolve it
127
129
  - naumu.ai/spaces/{slug} \u2192 naumu_list_graphs (resolve), then naumu_get_schema for an overview
128
130
  - naumu.ai/spaces/{slug}/views/{viewId} \u2192 naumu_list_graphs (resolve), then naumu_get_view, then naumu_list_view_nodes
129
131
  - 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
132
+ - naumu.ai/spaces/{slug}/chat/{threadId} \u2192 naumu_list_graphs (resolve), then naumu_read_thread
131
133
  - 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.
132
134
 
133
135
  Recommended workflow when a user pastes a view URL:
@@ -149,7 +151,7 @@ function registerListGraphs(server2, client2) {
149
151
  "naumu_list_graphs",
150
152
  {
151
153
  title: "List Graphs",
152
- annotations: { readOnlyHint: true },
154
+ annotations: { readOnlyHint: true, openWorldHint: false },
153
155
  description: "List all knowledge graphs (spaces) the authenticated user has access to. Returns graph IDs, names, and roles.",
154
156
  inputSchema: z.object({})
155
157
  },
@@ -169,8 +171,8 @@ function registerCreateGraph(server2, client2) {
169
171
  "naumu_create_graph",
170
172
  {
171
173
  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
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
175
+ description: "Create a new, empty knowledge graph (space) owned by the authenticated user; use when you need a fresh space to populate. Returns `{id, name, slug, role, memberRole, createdAt, onboardingThreadId}` - 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
176
  inputSchema: z2.object({
175
177
  name: z2.string().min(1).describe("Display name for the new space. A URL slug is generated from this name.")
176
178
  })
@@ -186,36 +188,42 @@ function registerCreateGraph(server2, client2) {
186
188
 
187
189
  // ../mcp-core/src/tools/get-schema.ts
188
190
  import { z as z3 } from "zod";
191
+ function formatConnection(c) {
192
+ const out = { relation: c.relation };
193
+ if (c.polymorphic) {
194
+ out.polymorphic = true;
195
+ } else if (c.target_node) {
196
+ out.target_node = c.target_node;
197
+ }
198
+ return out;
199
+ }
189
200
  function formatSchema(schema) {
190
201
  return {
191
202
  description: schema.description ?? null,
192
203
  types: schema.nodes.map((node) => {
193
204
  const result = { type: node.type };
194
205
  if (node.description) result.description = node.description;
206
+ const connections = {
207
+ required: (node.connections.required ?? []).map(formatConnection),
208
+ suggested: (node.connections.suggested ?? []).map(formatConnection)
209
+ };
195
210
  if (node.connections.parent) {
196
- result.parent = `${node.connections.parent.relation} \u2192 ${node.connections.parent.target_node}`;
211
+ connections.parent = formatConnection(node.connections.parent);
197
212
  }
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;
213
+ result.connections = connections;
203
214
  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
- );
215
+ result.attributes = node.attributes.map((a) => {
216
+ const values = (a.values ?? []).map((v) => {
217
+ const value = { label: v.label };
218
+ if (v.color) value.color = v.color;
219
+ if (v.description) value.description = v.description;
220
+ return value;
221
+ });
222
+ const attr = { name: a.name, values };
223
+ if (a.type) attr.type = a.type;
224
+ if (a.description) attr.description = a.description;
225
+ return attr;
226
+ });
219
227
  }
220
228
  return result;
221
229
  })
@@ -226,8 +234,8 @@ function registerGetSchema(server2, client2) {
226
234
  "naumu_get_schema",
227
235
  {
228
236
  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.",
237
+ annotations: { readOnlyHint: true, openWorldHint: false },
238
+ description: "Get the schema definition for a knowledge graph; call this before classifying a new node into a type, before picking an attribute value, or before extending the schema. Returns a ROUND-TRIPPABLE structural schema: each `types[]` entry has the SAME shape `naumu_update_schema` ingests (type, description, structured `connections: { parent?, required, suggested }` with parent NESTED - not a string, and structured attributes), so you can copy a type straight back into naumu_update_schema without losing data (notably the parent relation). 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
239
  inputSchema: z3.object({
232
240
  graphId: z3.string().describe("The graph ID")
233
241
  })
@@ -260,7 +268,7 @@ var ConnectionSchema = z4.object({
260
268
  var AttributeValueSchema = z4.object({
261
269
  label: z4.string().describe("Display label for this enum value"),
262
270
  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.')
271
+ description: z4.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won - deal signed and revenue committed"). Encouraged when the label alone is ambiguous.')
264
272
  });
265
273
  var AttributeSchema = z4.object({
266
274
  name: z4.string().describe('Attribute key (e.g. "stage", "status", "category")'),
@@ -277,10 +285,10 @@ var NodeTypeSchema = z4.object({
277
285
  }),
278
286
  attributes: z4.array(AttributeSchema).optional().describe("Type-level attributes for instances of this type."),
279
287
  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.'
288
+ '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 - it then falls back to the space-level defaultVisibility.'
281
289
  ),
282
290
  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.')
291
+ 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 - 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
292
  });
285
293
  var SchemaDefinitionSchema = z4.object({
286
294
  description: z4.string().optional().describe("Schema-level description / domain summary."),
@@ -291,8 +299,8 @@ function registerUpdateSchema(server2, client2) {
291
299
  "naumu_update_schema",
292
300
  {
293
301
  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.",
302
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
303
+ description: "Replace the graph schema with a new full definition; use to bootstrap an empty graph (cold-start) or fully replace mid-build. STRUCTURAL RULES (load-bearing): (1) HIERARCHICAL with a single root - 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 - never both, never a list of multiple targets. (3) DEFAULT TO CONCRETE target_node. Polymorphic is the ESCAPE HATCH - 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 - pick concrete relationships instead. If you can't pick a concrete parent, you may be missing a type - 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 - never add a self-relation just to enable nesting. (5) Always provide `description` on every node type, attribute, and select value - 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. When replacing mid-build, call naumu_get_schema first (it returns a round-trippable structural schema you can copy back) and send the FULL new schema; anything you omit is removed. PARENT PRESERVATION: omitting a type's `connections.parent` PRESERVES its existing parent (so a round-trip never silently orphans a type). To intentionally remove a parent and make a type a root, send `parent: null` explicitly. (A parent whose target type you delete is dropped automatically.) Conventions: type names PascalCase (Company, Person, Feature), relation names UPPER_SNAKE_CASE (WORKS_AT, BELONGS_TO). Bias toward general types - refine via attributes or nested children, not type proliferation.",
296
304
  inputSchema: z4.object({
297
305
  graphId: z4.string().describe("The graph ID"),
298
306
  schema: SchemaDefinitionSchema
@@ -332,15 +340,15 @@ function registerAddNodeType(server2, client2) {
332
340
  "naumu_add_node_type",
333
341
  {
334
342
  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.',
343
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
344
+ description: 'Add one new node type to the schema; use to extend an existing schema without resending the whole thing (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 - omit `parent`. Every subsequent type MUST set `parent`. (2) DEFAULT TO CONCRETE - pass {relation, target_node: <existing type>}. Polymorphic is the ESCAPE HATCH - 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 - add the missing type first instead of falling back to polymorphic. (3) Required/suggested connections also each take ONE concrete target_node OR polymorphic - 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` - 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 - use naumu_add_attribute / naumu_add_connection to extend it.',
337
345
  inputSchema: z5.object({
338
346
  graphId: z5.string(),
339
347
  type: z5.string().describe("PascalCase type name"),
340
348
  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."
349
+ "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 - agents reading the schema later rely on this to classify nodes."
342
350
  ),
343
- parent: ConnectionSchema2.optional().describe("Optional parent connection \u2014 sets this type as a child of another type."),
351
+ parent: ConnectionSchema2.optional().describe("Optional parent connection - sets this type as a child of another type."),
344
352
  required: z5.array(ConnectionSchema2).optional(),
345
353
  suggested: z5.array(ConnectionSchema2).optional(),
346
354
  attributes: z5.array(AttributeSchema2).optional(),
@@ -386,8 +394,8 @@ function registerAddConnection(server2, client2) {
386
394
  "naumu_add_connection",
387
395
  {
388
396
  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.',
397
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
398
+ description: 'Add one connection from an existing node type to another; use to extend a type\'s relations without resending the whole schema. `kind`: "parent" (sets/replaces - 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 - never both. DEFAULT TO CONCRETE target_node. Polymorphic is the ESCAPE HATCH - 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 - add it first instead of falling back to polymorphic. Same-type nesting (e.g. Topic under Topic) does NOT need a new connection - use the existing parent relation. Relation name UPPER_SNAKE_CASE.',
391
399
  inputSchema: z6.object({
392
400
  graphId: z6.string(),
393
401
  source_type: z6.string().describe("Existing node type to add the connection to."),
@@ -436,8 +444,8 @@ function registerAddAttribute(server2, client2) {
436
444
  "naumu_add_attribute",
437
445
  {
438
446
  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.',
447
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
448
+ description: 'Add or extend an attribute on an existing node type; use to add a field or merge new enum values into an existing one. 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: [] - 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 - short, contrastive notes (e.g. attribute "stage - sales funnel position; distinct from status which captures health/blockers", value "closed-won - deal signed and revenue committed"). Descriptions are the single strongest signal future agents use when picking which attribute/value to set.',
441
449
  inputSchema: z7.object({
442
450
  graphId: z7.string(),
443
451
  node_type: z7.string().describe("Existing node type to add the attribute to."),
@@ -447,7 +455,7 @@ function registerAddAttribute(server2, client2) {
447
455
  z7.object({
448
456
  label: z7.string(),
449
457
  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.')
458
+ description: z7.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won - deal signed and revenue committed"). Encouraged when the label alone is ambiguous.')
451
459
  })
452
460
  ).default([]),
453
461
  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.')
@@ -490,8 +498,8 @@ function registerSearch(server2, client2) {
490
498
  "naumu_search",
491
499
  {
492
500
  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.',
501
+ annotations: { readOnlyHint: true, openWorldHint: false },
502
+ description: 'Hybrid search over graph nodes; use for meaning-based lookup when you don\'t know the exact label. 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 - `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 - the fusion handles both.',
495
503
  inputSchema: z8.object({
496
504
  graphId: z8.string().describe("The graph ID"),
497
505
  query: z8.string().describe(
@@ -521,15 +529,15 @@ function registerFilter(server2, client2) {
521
529
  "naumu_filter",
522
530
  {
523
531
  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.',
532
+ annotations: { readOnlyHint: true, openWorldHint: false },
533
+ description: 'Filter nodes by type and attributes with deterministic, complete results; use 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) - no semantic ranking, no missed results. Results are sorted by sortKey or recency.',
526
534
  inputSchema: z9.object({
527
535
  graphId: z9.string().describe("The graph ID"),
528
536
  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(
537
+ includeAttributes: z9.record(z9.string(), z9.array(z9.string())).optional().describe(
530
538
  'Only include nodes where attribute matches one of the values. Example: {"Status": ["Todo", "In Progress"]}'
531
539
  ),
532
- excludeAttributes: z9.record(z9.array(z9.string())).optional().describe(
540
+ excludeAttributes: z9.record(z9.string(), z9.array(z9.string())).optional().describe(
533
541
  'Exclude nodes where attribute matches any of the values. Example: {"Status": ["Done", "Wont do"]}'
534
542
  ),
535
543
  sortBy: z9.enum(["sortKey", "updatedAt", "label"]).optional().default("sortKey").describe('Sort order: "sortKey" (default), "updatedAt" (most recent first), or "label" (alphabetical)'),
@@ -564,7 +572,7 @@ function registerGetNode(server2, client2) {
564
572
  "naumu_get_node",
565
573
  {
566
574
  title: "Get Node",
567
- annotations: { readOnlyHint: true },
575
+ annotations: { readOnlyHint: true, openWorldHint: false },
568
576
  description: "Get a single node with all its properties and connections (incoming and outgoing edges).",
569
577
  inputSchema: z10.object({
570
578
  graphId: z10.string().describe("The graph ID"),
@@ -585,7 +593,7 @@ import { z as z11 } from "zod";
585
593
  var NodeInput = z11.object({
586
594
  label: z11.string().describe("Display name of the node"),
587
595
  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."),
596
+ content: z11.string().min(1).describe("Rich text content / description. REQUIRED - every node must explain what it is."),
589
597
  attributes: z11.record(z11.string(), z11.unknown()).optional().describe("Additional key-value attributes")
590
598
  });
591
599
  function registerAddNode(server2, client2) {
@@ -593,8 +601,8 @@ function registerAddNode(server2, client2) {
593
601
  "naumu_add_node",
594
602
  {
595
603
  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.',
604
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
605
+ description: 'Create 1-25 nodes in the knowledge graph in a single call; use when you have a vetted, dedup-checked batch ready to insert. Keep batches small and atomic (5-25 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"}` - there is NO server-side dedup, every input becomes a node. Dedup is the caller\'s responsibility: BEFORE calling this tool, run `naumu_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_delegate` for general knowledge intake - it discovers and creates connections for you.',
598
606
  inputSchema: z11.object({
599
607
  graphId: z11.string().describe("The graph ID"),
600
608
  nodes: z11.array(NodeInput).min(1).max(25).describe("Batch of 1\u201325 nodes to create. Keep batches small for atomicity.")
@@ -670,8 +678,8 @@ function registerUpdateNode(server2, client2) {
670
678
  "naumu_update_node",
671
679
  {
672
680
  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.',
681
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
682
+ description: 'Overwrite properties of an existing node; use for simple attribute changes like setting a status, priority, or due date. Only the provided fields will be changed. For content or structural changes, consider `naumu_delegate` instead - 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
683
  inputSchema: z12.object({
676
684
  graphId: z12.string().describe("The graph ID"),
677
685
  nodeId: z12.string().describe("The node ID to update"),
@@ -757,8 +765,8 @@ function registerAddEdge(server2, client2) {
757
765
  "naumu_add_edge",
758
766
  {
759
767
  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.",
768
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
769
+ description: "Create 1-25 relationships (edges) between existing nodes in a single call; use after a bulk node insert to wire nodes together. Keep batches small and atomic (5-25 edges) so failures stay contained. Typical workflow: 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 or drop offending edges before calling. Prefer `naumu_delegate` when you want the agent to discover the right connections itself.",
762
770
  inputSchema: z13.object({
763
771
  graphId: z13.string().describe("The graph ID"),
764
772
  edges: z13.array(EdgeInput).min(1).max(25).describe("Batch of 1\u201325 edges to create. Keep batches small for atomicity.")
@@ -786,8 +794,8 @@ function registerRemoveNode(server2, client2) {
786
794
  "naumu_remove_node",
787
795
  {
788
796
  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.",
797
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
798
+ description: "Delete a node and all its connections from the knowledge graph; use only when you need precise, surgical deletion. This is destructive. Prefer `naumu_delegate` for removing knowledge - it understands the impact on the broader graph and can handle cascading changes.",
791
799
  inputSchema: z14.object({
792
800
  graphId: z14.string().describe("The graph ID"),
793
801
  nodeId: z14.string().describe("The node ID to delete")
@@ -809,8 +817,8 @@ function registerRemoveEdge(server2, client2) {
809
817
  "naumu_remove_edge",
810
818
  {
811
819
  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.",
820
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
821
+ description: "Delete a single edge identified by `(source, target, label)` tuple; use to correct a wrong-target edge mistake (a recovery tool, not a routine one). Does NOT delete the endpoint nodes - 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 - call `naumu_reparent` first if you want connectivity preserved. Response: `{deleted: 0 | 1, warnings: string[]}`. Prefer `naumu_delegate` when the structural intent is broader than removing one specific edge - it can reason about the surrounding graph.",
814
822
  inputSchema: z15.object({
815
823
  graphId: z15.string().describe("The graph ID"),
816
824
  source: z15.string().describe("Source node id of the edge to delete"),
@@ -843,11 +851,11 @@ function registerRemoveEdgesBulk(server2, client2) {
843
851
  "naumu_remove_edges_bulk",
844
852
  {
845
853
  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.",
854
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
855
+ description: "Delete 1-100 edges in a single atomic call (all-or-none); use to fix multiple wrong-target edge mistakes in one shot. Does NOT delete endpoint nodes. System edges (HAS_NODE, HAS_THREAD, OWNS, etc.) are rejected - 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[]}`) - call `naumu_reparent` first if you want connectivity preserved. Prefer `naumu_delegate` for broader structural cleanup that needs graph-wide reasoning.",
848
856
  inputSchema: z16.object({
849
857
  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.")
858
+ edges: z16.array(EdgeRef).min(1).max(100).describe("1-100 edges to delete. Atomic per call - all succeed or none do.")
851
859
  })
852
860
  },
853
861
  async ({ graphId, edges }) => {
@@ -865,95 +873,108 @@ function registerAsk(server2, client2) {
865
873
  server2.registerTool(
866
874
  "naumu_ask",
867
875
  {
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.',
876
+ title: "Ask Naumu",
877
+ // Writes a visible conversation (question + reply) and runs the @Naumu
878
+ // agent. Additive, not destructive. Open-world: the answer is
879
+ // synthesised by an LLM with full read access to the space.
880
+ annotations: {
881
+ readOnlyHint: false,
882
+ destructiveHint: false,
883
+ idempotentHint: false,
884
+ openWorldHint: true
885
+ },
886
+ description: 'Ask @Naumu a question about a space and get a synthesised answer back, with the node ids it drew from and a confidence hint. This is the primary, go-to tool for getting information about a space: what is in it, what is new or recently changed, how something works, or a summary of any topic. @Naumu has full read access and assembles the answer for you, so prefer it for any space question and fall back to the granular reads only when you need precision it cannot give (naumu_search to find nodes by meaning, naumu_filter for every node of a type/attribute, naumu_list_threads + naumu_read_thread for conversation history). The question and answer are saved as a visible conversation in the space. Returns { answer, sources, confidence, threadId, status }; on a long synthesis it returns status "processing" with a threadId you read later via naumu_read_thread. To hand @Naumu work to carry out (add knowledge, make changes, record status) without waiting, use naumu_delegate instead.',
871
887
  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)")
888
+ graphId: z17.string().describe("The space (graph) id to ask about."),
889
+ question: z17.string().max(4e3).describe("The question for @Naumu. Up to 4000 characters.")
875
890
  })
876
891
  },
877
- async ({ graphId, question, threadId }) => {
878
- if (!threadId) {
879
- const thread = await client2.post("/api/threads", { graphId });
880
- threadId = thread.id;
892
+ async ({ graphId, question }) => {
893
+ try {
894
+ const data = await client2.post(`/api/graphs/${graphId}/ask`, { question });
895
+ return {
896
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
897
+ };
898
+ } catch (err) {
899
+ const message = err instanceof Error ? err.message : String(err);
900
+ return {
901
+ content: [{ type: "text", text: `Error: ${message}` }],
902
+ isError: true
903
+ };
881
904
  }
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
905
  }
895
906
  );
896
907
  }
897
908
 
898
- // ../mcp-core/src/tools/get-ai-thread.ts
909
+ // ../mcp-core/src/tools/delegate.ts
899
910
  import { z as z18 } from "zod";
900
- function registerGetAiThread(server2, client2) {
911
+ function registerDelegate(server2, client2) {
901
912
  server2.registerTool(
902
- "naumu_get_ai_thread",
913
+ "naumu_delegate",
903
914
  {
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.',
915
+ title: "Delegate to Naumu",
916
+ // Posts a message that the @Naumu agent acts on in the background.
917
+ // Additive, not destructive at this layer. Open-world: hands work to an
918
+ // LLM agent that may read and write the graph.
919
+ annotations: {
920
+ readOnlyHint: false,
921
+ destructiveHint: false,
922
+ idempotentHint: false,
923
+ openWorldHint: true
924
+ },
925
+ description: 'Hand @Naumu a task to carry out asynchronously: add knowledge, make graph-aware changes, or record a status update (e.g. "mark task X done", "log this deployment"). Returns immediately with a threadId; @Naumu works in the background and may take seconds to minutes. For status reports you can fire and forget. To read what @Naumu did, poll naumu_read_thread with the returned threadId. When you instead need an answer synchronously, use naumu_ask.',
907
926
  inputSchema: z18.object({
908
- threadId: z18.string().describe("The thread ID returned by naumu_ask")
927
+ graphId: z18.string().describe("The space (graph) id to act in."),
928
+ task: z18.string().describe("What you want @Naumu to do, add, or record."),
929
+ threadId: z18.string().optional().describe("Continue an existing conversation; omit to start a new one.")
909
930
  })
910
931
  },
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
- };
932
+ async ({ graphId, task, threadId }) => {
933
+ try {
934
+ let resolvedThreadId = threadId;
935
+ if (!resolvedThreadId) {
936
+ const thread = await client2.post("/api/threads", { graphId });
937
+ resolvedThreadId = thread.id;
938
+ }
939
+ await client2.post(`/api/threads/${resolvedThreadId}/messages`, {
940
+ content: task,
941
+ async: true
942
+ });
943
+ return {
944
+ content: [
945
+ {
946
+ type: "text",
947
+ text: JSON.stringify(
948
+ { threadId: resolvedThreadId, status: "processing" },
949
+ null,
950
+ 2
951
+ )
952
+ }
953
+ ]
954
+ };
955
+ } catch (err) {
956
+ const message = err instanceof Error ? err.message : String(err);
957
+ return {
958
+ content: [{ type: "text", text: `Error: ${message}` }],
959
+ isError: true
960
+ };
961
+ }
941
962
  }
942
963
  );
943
964
  }
944
965
 
945
966
  // ../mcp-core/src/tools/get-view.ts
946
- import { z as z20 } from "zod";
967
+ import { z as z19 } from "zod";
947
968
  function registerGetView(server2, client2) {
948
969
  server2.registerTool(
949
970
  "naumu_get_view",
950
971
  {
951
972
  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_").')
973
+ annotations: { readOnlyHint: true, openWorldHint: false },
974
+ 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. 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.",
975
+ inputSchema: z19.object({
976
+ graphId: z19.string().describe("The graph (space) ID. From naumu.ai URLs this is the value after /spaces/."),
977
+ viewId: z19.string().describe('The view ID (typically prefixed with "view_").')
957
978
  })
958
979
  },
959
980
  async ({ graphId, viewId }) => {
@@ -966,20 +987,20 @@ function registerGetView(server2, client2) {
966
987
  }
967
988
 
968
989
  // ../mcp-core/src/tools/list-view-nodes.ts
969
- import { z as z21 } from "zod";
990
+ import { z as z20 } from "zod";
970
991
  function registerListViewNodes(server2, client2) {
971
992
  server2.registerTool(
972
993
  "naumu_list_view_nodes",
973
994
  {
974
995
  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".')
996
+ annotations: { readOnlyHint: true, openWorldHint: false },
997
+ description: 'List nodes that match a saved view; pair with naumu_get_view first to read the view\'s filters and decide what payload size you need. Cursor-paginated.\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} - best for browsing.\n- "id": {id} only - use when you need to count or iterate cheaply.\n- "full": full node payload with all attributes - use when you need every property.\n\nDefault page size 25, max 100.',
998
+ inputSchema: z20.object({
999
+ graphId: z20.string().describe("The graph (space) ID."),
1000
+ viewId: z20.string().describe("The view ID."),
1001
+ cursor: z20.string().optional().describe("Opaque cursor from a previous response's nextCursor. Omit for the first page."),
1002
+ limit: z20.number().int().min(1).max(100).optional().describe("Page size, default 25, max 100."),
1003
+ fields: z20.enum(["id", "summary", "full"]).optional().describe('How much detail per node. Default "summary".')
983
1004
  })
984
1005
  },
985
1006
  async ({ graphId, viewId, cursor, limit, fields }) => {
@@ -998,16 +1019,16 @@ function registerListViewNodes(server2, client2) {
998
1019
  }
999
1020
 
1000
1021
  // ../mcp-core/src/tools/list-canvases.ts
1001
- import { z as z22 } from "zod";
1022
+ import { z as z21 } from "zod";
1002
1023
  function registerListCanvases(server2, client2) {
1003
1024
  server2.registerTool(
1004
1025
  "naumu_list_canvases",
1005
1026
  {
1006
1027
  title: "List Canvases",
1007
- annotations: { readOnlyHint: true },
1028
+ annotations: { readOnlyHint: true, openWorldHint: false },
1008
1029
  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")
1030
+ inputSchema: z21.object({
1031
+ graphId: z21.string().describe("The graph ID to list canvases for")
1011
1032
  })
1012
1033
  },
1013
1034
  async ({ graphId }) => {
@@ -1020,16 +1041,16 @@ function registerListCanvases(server2, client2) {
1020
1041
  }
1021
1042
 
1022
1043
  // ../mcp-core/src/tools/get-canvas-elements.ts
1023
- import { z as z23 } from "zod";
1044
+ import { z as z22 } from "zod";
1024
1045
  function registerGetCanvasElements(server2, client2) {
1025
1046
  server2.registerTool(
1026
1047
  "naumu_get_canvas_elements",
1027
1048
  {
1028
1049
  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")
1050
+ annotations: { readOnlyHint: true, openWorldHint: false },
1051
+ description: "Read the structured JSON contents of a canvas - 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.",
1052
+ inputSchema: z22.object({
1053
+ canvasId: z22.string().describe("The canvas ID")
1033
1054
  })
1034
1055
  },
1035
1056
  async ({ canvasId }) => {
@@ -1042,21 +1063,21 @@ function registerGetCanvasElements(server2, client2) {
1042
1063
  }
1043
1064
 
1044
1065
  // ../mcp-core/src/tools/get-canvas-image.ts
1045
- import { z as z24 } from "zod";
1066
+ import { z as z23 } from "zod";
1046
1067
  var MAX_INLINE_BYTES = 4 * 1024 * 1024;
1047
1068
  function registerGetCanvasImage(server2, client2) {
1048
1069
  server2.registerTool(
1049
1070
  "naumu_get_canvas_image",
1050
1071
  {
1051
1072
  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."
1073
+ annotations: { readOnlyHint: true, openWorldHint: false },
1074
+ description: 'Render a canvas to a PNG image so you can visually inspect what was drawn - 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).',
1075
+ inputSchema: z23.object({
1076
+ canvasId: z23.string().describe("The canvas ID"),
1077
+ scale: z23.union([z23.literal(1), z23.literal(2)]).default(2).describe("Pixel density (1 or 2). Default is 2 for retina-quality output."),
1078
+ theme: z23.enum(["light", "dark"]).default("light").describe("Background theme to render with."),
1079
+ mode: z23.enum(["inline", "url"]).default("inline").describe(
1080
+ "`inline` (default) embeds the PNG directly so vision-capable hosts see it. `url` returns a signed S3 URL that expires in 1 hour - useful for large canvases or sharing."
1060
1081
  )
1061
1082
  })
1062
1083
  },
@@ -1119,19 +1140,26 @@ function formatBytes(bytes) {
1119
1140
  }
1120
1141
 
1121
1142
  // ../mcp-core/src/tools/post-message.ts
1122
- import { z as z25 } from "zod";
1143
+ import { z as z24 } from "zod";
1123
1144
  function registerPostMessage(server2, client2) {
1124
1145
  server2.registerTool(
1125
1146
  "naumu_post_message",
1126
1147
  {
1127
1148
  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.")
1149
+ // Additive: appends a message to a thread. Not destructive, not
1150
+ // idempotent, graph-local.
1151
+ annotations: {
1152
+ readOnlyHint: false,
1153
+ destructiveHint: false,
1154
+ idempotentHint: false,
1155
+ openWorldHint: false
1156
+ },
1157
+ description: 'Post a message in a Naumu thread you participate in. Use it 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". @mentioning people loops them in without invoking @Naumu. To attach files call naumu_request_attachment_upload first, PUT the bytes to the returned uploadUrl, then pass the resulting attachmentIds here. The message needs either `content` or `attachmentIds`. Returns the created message JSON. To get a synthesised answer from @Naumu, use naumu_ask.',
1158
+ inputSchema: z24.object({
1159
+ threadId: z24.string().describe("The thread ID to post into. You must be a participant in this thread."),
1160
+ content: z24.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.'),
1161
+ contentFormat: z24.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 } }`.'),
1162
+ attachmentIds: z24.array(z24.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
1163
  })
1136
1164
  },
1137
1165
  async ({ threadId, content, contentFormat, attachmentIds }) => {
@@ -1157,18 +1185,18 @@ function registerPostMessage(server2, client2) {
1157
1185
  }
1158
1186
 
1159
1187
  // ../mcp-core/src/tools/read-thread.ts
1160
- import { z as z26 } from "zod";
1188
+ import { z as z25 } from "zod";
1161
1189
  function registerReadThread(server2, client2) {
1162
1190
  server2.registerTool(
1163
1191
  "naumu_read_thread",
1164
1192
  {
1165
1193
  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.")
1194
+ annotations: { readOnlyHint: true, openWorldHint: false },
1195
+ description: 'Read messages from a Naumu thread. Returns paginated history ordered newest-first; each message carries a `status` (`processing` while @Naumu is still composing, `complete` when done). Use this to pick up an answer after naumu_ask returns status "processing", or to read what naumu_delegate produced. Use `before` (timestamp ms) to page further back. Default page size 50, max 200.',
1196
+ inputSchema: z25.object({
1197
+ threadId: z25.string().describe("The thread ID to read from."),
1198
+ before: z25.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
1199
+ limit: z25.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1172
1200
  })
1173
1201
  },
1174
1202
  async ({ threadId, before, limit }) => {
@@ -1193,65 +1221,16 @@ function registerReadThread(server2, client2) {
1193
1221
  );
1194
1222
  }
1195
1223
 
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) {
1235
- const message = err instanceof Error ? err.message : String(err);
1236
- return {
1237
- content: [{ type: "text", text: `Error: ${message}` }],
1238
- isError: true
1239
- };
1240
- }
1241
- }
1242
- );
1243
- }
1244
-
1245
1224
  // ../mcp-core/src/tools/whoami.ts
1246
- import { z as z28 } from "zod";
1225
+ import { z as z26 } from "zod";
1247
1226
  function registerWhoami(server2, client2, allToolNames) {
1248
1227
  server2.registerTool(
1249
1228
  "naumu_whoami",
1250
1229
  {
1251
1230
  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({})
1231
+ annotations: { readOnlyHint: true, openWorldHint: false },
1232
+ 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 - a person spans many graphs, so resolve a specific graph via naumu_list_graphs. No arguments. Always available regardless of the permission grid.',
1233
+ inputSchema: z26.object({})
1255
1234
  },
1256
1235
  async () => {
1257
1236
  try {
@@ -1274,18 +1253,25 @@ function registerWhoami(server2, client2, allToolNames) {
1274
1253
  }
1275
1254
 
1276
1255
  // ../mcp-core/src/tools/list-threads.ts
1277
- import { z as z29 } from "zod";
1256
+ import { z as z27 } from "zod";
1257
+ function sanitizeThreadParticipants(thread) {
1258
+ if (!thread || typeof thread !== "object" || !("participantEmails" in thread)) {
1259
+ return thread;
1260
+ }
1261
+ const { participantEmails: _participantEmails, ...rest } = thread;
1262
+ return rest;
1263
+ }
1278
1264
  function registerListThreads(server2, client2) {
1279
1265
  server2.registerTool(
1280
1266
  "naumu_list_threads",
1281
1267
  {
1282
1268
  title: "List Threads",
1283
- annotations: { readOnlyHint: true },
1269
+ annotations: { readOnlyHint: true, openWorldHint: false },
1284
1270
  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.")
1271
+ inputSchema: z27.object({
1272
+ graphId: z27.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1273
+ cursor: z27.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1274
+ limit: z27.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1289
1275
  })
1290
1276
  },
1291
1277
  async ({ graphId, cursor, limit }) => {
@@ -1302,8 +1288,12 @@ function registerListThreads(server2, client2) {
1302
1288
  path = `/api/identities/me/threads${qs ? `?${qs}` : ""}`;
1303
1289
  }
1304
1290
  const data = await client2.get(path);
1291
+ const clean = data && typeof data === "object" && Array.isArray(data.threads) ? {
1292
+ ...data,
1293
+ threads: data.threads.map(sanitizeThreadParticipants)
1294
+ } : data;
1305
1295
  return {
1306
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1296
+ content: [{ type: "text", text: JSON.stringify(clean, null, 2) }]
1307
1297
  };
1308
1298
  } catch (err) {
1309
1299
  const message = err instanceof Error ? err.message : String(err);
@@ -1317,23 +1307,31 @@ function registerListThreads(server2, client2) {
1317
1307
  }
1318
1308
 
1319
1309
  // ../mcp-core/src/tools/get-thread.ts
1320
- import { z as z30 } from "zod";
1310
+ import { z as z28 } from "zod";
1311
+ function sanitizeThreadParticipants2(thread) {
1312
+ if (!thread || typeof thread !== "object" || !("participantEmails" in thread)) {
1313
+ return thread;
1314
+ }
1315
+ const { participantEmails: _participantEmails, ...rest } = thread;
1316
+ return rest;
1317
+ }
1321
1318
  function registerGetThread(server2, client2) {
1322
1319
  server2.registerTool(
1323
1320
  "naumu_get_thread",
1324
1321
  {
1325
1322
  title: "Get Thread",
1326
- annotations: { readOnlyHint: true },
1323
+ annotations: { readOnlyHint: true, openWorldHint: false },
1327
1324
  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.")
1325
+ inputSchema: z28.object({
1326
+ threadId: z28.string().describe("The thread ID to fetch.")
1330
1327
  })
1331
1328
  },
1332
1329
  async ({ threadId }) => {
1333
1330
  try {
1334
1331
  const data = await client2.get(`/api/threads/${threadId}`);
1332
+ const clean = sanitizeThreadParticipants2(data);
1335
1333
  return {
1336
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1334
+ content: [{ type: "text", text: JSON.stringify(clean, null, 2) }]
1337
1335
  };
1338
1336
  } catch (err) {
1339
1337
  const message = err instanceof Error ? err.message : String(err);
@@ -1347,30 +1345,37 @@ function registerGetThread(server2, client2) {
1347
1345
  }
1348
1346
 
1349
1347
  // ../mcp-core/src/tools/create-thread.ts
1350
- import { z as z31 } from "zod";
1348
+ import { z as z29 } from "zod";
1351
1349
  function registerCreateThread(server2, client2) {
1352
1350
  server2.registerTool(
1353
1351
  "naumu_create_thread",
1354
1352
  {
1355
1353
  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.")
1354
+ // Additive: creates a thread (and optional first message). Not
1355
+ // destructive, not idempotent, graph-local.
1356
+ annotations: {
1357
+ readOnlyHint: false,
1358
+ destructiveHint: false,
1359
+ idempotentHint: false,
1360
+ openWorldHint: false
1361
+ },
1362
+ description: "Start a new conversation in a space. You are auto-attached as a participant, and the thread's formal creator is your primary owner (the user who registered you), so it shows in their sidebar. Optional `participants` adds humans (by userId) and other bots (by identityId) at creation. Optional `initialMessage` opens the conversation as your first message. Tagging people loops them in without invoking @Naumu; only an explicit @Naumu mention, or naumu_ask, brings the agent in. Returns the created thread (including its id) so you can follow up with naumu_post_message.",
1363
+ inputSchema: z29.object({
1364
+ title: z29.string().min(1).max(200).optional().describe('Thread title shown in the sidebar. If omitted, Naumu generates a default like "Conversation YYYY-MM-DD".'),
1365
+ participants: z29.array(
1366
+ z29.discriminatedUnion("type", [
1367
+ z29.object({
1368
+ type: z29.literal("user"),
1369
+ userId: z29.string().min(1).describe("User UUID \u2014 get these from `naumu_get_thread`/`naumu_read_thread` participant rosters or webhook payloads.")
1365
1370
  }),
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.")
1371
+ z29.object({
1372
+ type: z29.literal("identity"),
1373
+ identityId: z29.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
1374
  })
1370
1375
  ])
1371
1376
  ).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.")
1377
+ initialMessage: z29.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."),
1378
+ visibility: z29.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
1379
  })
1375
1380
  },
1376
1381
  async ({ title, participants, initialMessage, visibility }) => {
@@ -1396,20 +1401,20 @@ function registerCreateThread(server2, client2) {
1396
1401
  }
1397
1402
 
1398
1403
  // ../mcp-core/src/tools/request-attachment-upload.ts
1399
- import { z as z32 } from "zod";
1404
+ import { z as z30 } from "zod";
1400
1405
  function registerRequestAttachmentUpload(server2, client2) {
1401
1406
  server2.registerTool(
1402
1407
  "naumu_request_attachment_upload",
1403
1408
  {
1404
1409
  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).")
1410
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1411
+ description: 'Request a presigned S3 upload URL to attach a file to a message; use when you want to deliver generated content as a file (a markdown report, a PDF, an image, an audio recording, a video). 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`. 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 - the URL itself is the auth.\n\u2022 Do NOT log `uploadUrl` - 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 - you cannot reuse an upload across threads.',
1412
+ inputSchema: z30.object({
1413
+ threadId: z30.string().describe("Thread the attachment will land in. You must be a participant. The pending attachment is keyed to this thread - you cannot reuse it for a different one."),
1414
+ fileName: z30.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."),
1415
+ fileType: z30.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."),
1416
+ fileSize: z30.number().int().positive().describe("File size in bytes. Validated against per-MIME caps before the URL is issued - exceeding the cap returns a 400."),
1417
+ audioDurationSec: z30.number().positive().optional().describe("For audio attachments, duration in seconds. Validated against the audio recording cap (currently 8 hours).")
1413
1418
  })
1414
1419
  },
1415
1420
  async ({ threadId, fileName, fileType, fileSize, audioDurationSec }) => {
@@ -1439,18 +1444,18 @@ function registerRequestAttachmentUpload(server2, client2) {
1439
1444
  }
1440
1445
 
1441
1446
  // ../mcp-core/src/tools/add-reaction.ts
1442
- import { z as z33 } from "zod";
1447
+ import { z as z31 } from "zod";
1443
1448
  function registerAddReaction(server2, client2) {
1444
1449
  server2.registerTool(
1445
1450
  "naumu_add_reaction",
1446
1451
  {
1447
1452
  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.')
1453
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1454
+ description: 'Add an emoji reaction to a message in a thread you are participating in; use for lightweight acknowledgement instead of posting a message. Idempotent - calling twice with the same emoji is a no-op (use `naumu_remove_reaction` to undo). Returns `{ ok, messageId, emoji, alreadyExisted, reactionCount, reactions }` so you can confirm the state without re-reading the thread; `alreadyExisted: true` means the reaction was already on the message and the call was a no-op.\n\nWhen to react vs. when to post a message:\n\u2022 React (no message) for lightweight acknowledgement (\u{1F440}, \u2705, \u{1F44D}), appreciation (\u2764\uFE0F, \u{1F64C}), laughter (\u{1F602}), or "I saw this".\n\u2022 Post a message for direct questions, clarification, important corrections, or final results - situations where words are required.\n\u2022 For long tasks: react \u{1F440} first to acknowledge, optionally post a short "On it - I\'ll report back" if the work will take >20s, do the work, then post the final result.\n\u2022 Ignore casual human banter, side-conversations someone else already answered, or anything where you would only say "ok"/"nice"/"lol".\n\nUse at most one reaction per message unless explicitly useful. Reactions are social backpressure relief, not a sparkle-confetti channel.',
1455
+ inputSchema: z31.object({
1456
+ threadId: z31.string().describe("Thread containing the message. You must be a participant."),
1457
+ messageId: z31.string().describe("The message to react to."),
1458
+ emoji: z31.string().min(1).describe('Emoji character (e.g. "\u{1F440}", "\u2705", "\u2764\uFE0F"). Custom-emoji shortcodes are NOT supported here - pass a real Unicode emoji.')
1454
1459
  })
1455
1460
  },
1456
1461
  async ({ threadId, messageId, emoji }) => {
@@ -1474,18 +1479,18 @@ function registerAddReaction(server2, client2) {
1474
1479
  }
1475
1480
 
1476
1481
  // ../mcp-core/src/tools/remove-reaction.ts
1477
- import { z as z34 } from "zod";
1482
+ import { z as z32 } from "zod";
1478
1483
  function registerRemoveReaction(server2, client2) {
1479
1484
  server2.registerTool(
1480
1485
  "naumu_remove_reaction",
1481
1486
  {
1482
1487
  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).")
1488
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1489
+ description: "Remove your own emoji reaction from a message; use to walk back an acknowledgement you previously added. Idempotent - calling on a reaction you never added is a no-op. Pair with `naumu_add_reaction` (e.g. you reacted \u{1F440} to start a task and want to clear it after a final result message lands). Returns `{ ok, messageId, emoji, alreadyExisted, reactionCount, reactions }` - `alreadyExisted: false` means there was nothing to remove and the call was a no-op.",
1490
+ inputSchema: z32.object({
1491
+ threadId: z32.string().describe("Thread containing the message. You must be a participant."),
1492
+ messageId: z32.string().describe("The message to remove your reaction from."),
1493
+ emoji: z32.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1489
1494
  })
1490
1495
  },
1491
1496
  async ({ threadId, messageId, emoji }) => {
@@ -1509,19 +1514,20 @@ function registerRemoveReaction(server2, client2) {
1509
1514
  }
1510
1515
 
1511
1516
  // ../mcp-core/src/tools/naumu-typing.ts
1512
- import { z as z35 } from "zod";
1517
+ import { z as z33 } from "zod";
1513
1518
  function registerNaumuTyping(server2, client2) {
1514
1519
  server2.registerTool(
1515
1520
  "naumu_typing",
1516
1521
  {
1517
1522
  title: "Set Typing Indicator",
1518
1523
  // 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.')
1524
+ // state, self-expires, trivially reversible via "stop"). Not destructive;
1525
+ // repeating the same state is a no-op renew, so idempotent.
1526
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1527
+ description: 'Show or hide your "is typing\u2026" pill in a thread; use to signal that you are composing a reply. Call with `state: "start"` the moment you decide to compose a reply (before any LLM call), and the server holds the pill alive - re-broadcasting on a short interval - until you stop, post a message, or the lease cap (~5 min) fires. You do NOT need to refresh on a timer; that\'s the lease\'s job.\n\nThe pill clears automatically when:\n\u2022 you call this tool with `state: "stop"`\n\u2022 you call `naumu_post_message` for the same thread (cleared on commit)\n\u2022 the lease cap expires\n\nUse `start` whenever you start work, even if you might end up not replying - call `stop` if you decide NOT to post. Calling `start` while a lease is already active renews it (resets the cap), so a long-running run can call `start` again as a heartbeat without breaking the indicator. You must be a participant of the thread.',
1528
+ inputSchema: z33.object({
1529
+ threadId: z33.string().describe("The thread ID to set typing in. You must be a participant."),
1530
+ state: z33.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1525
1531
  })
1526
1532
  },
1527
1533
  async ({ threadId, state }) => {
@@ -1542,16 +1548,16 @@ function registerNaumuTyping(server2, client2) {
1542
1548
  }
1543
1549
 
1544
1550
  // ../mcp-core/src/tools/note-read.ts
1545
- import { z as z36 } from "zod";
1551
+ import { z as z34 } from "zod";
1546
1552
  function registerNoteRead(server2, client2) {
1547
1553
  server2.registerTool(
1548
1554
  "naumu_note_read",
1549
1555
  {
1550
1556
  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")
1557
+ annotations: { readOnlyHint: true, openWorldHint: false },
1558
+ description: "Read the current contents of a note as markdown; use before editing so you know what you're working with. `naumu_note_find_replace` and the section-based tools (`naumu_note_insert`, `naumu_note_replace_section`, `naumu_note_delete_section`) anchor on text/headings present in the live doc.",
1559
+ inputSchema: z34.object({
1560
+ noteId: z34.string().describe("The note (Thought) ID")
1555
1561
  })
1556
1562
  },
1557
1563
  async ({ noteId }) => {
@@ -1564,17 +1570,17 @@ function registerNoteRead(server2, client2) {
1564
1570
  }
1565
1571
 
1566
1572
  // ../mcp-core/src/tools/note-append.ts
1567
- import { z as z37 } from "zod";
1573
+ import { z as z35 } from "zod";
1568
1574
  function registerNoteAppend(server2, client2) {
1569
1575
  server2.registerTool(
1570
1576
  "naumu_note_append",
1571
1577
  {
1572
1578
  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")
1579
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1580
+ description: "Append markdown blocks to the end of a note; use for additive note writing that never touches existing content. Other participants see your colored cursor while the write lands. Markdown supports headings (1-3), bold/italic/code, lists, blockquotes, code blocks, links, and tables.",
1581
+ inputSchema: z35.object({
1582
+ noteId: z35.string().describe("The note (Thought) ID to append to"),
1583
+ markdown: z35.string().min(1).describe("Markdown content to append at the end of the note")
1578
1584
  })
1579
1585
  },
1580
1586
  async ({ noteId, markdown }) => {
@@ -1587,18 +1593,18 @@ function registerNoteAppend(server2, client2) {
1587
1593
  }
1588
1594
 
1589
1595
  // ../mcp-core/src/tools/note-insert.ts
1590
- import { z as z38 } from "zod";
1596
+ import { z as z36 } from "zod";
1591
1597
  function registerNoteInsert(server2, client2) {
1592
1598
  server2.registerTool(
1593
1599
  "naumu_note_insert",
1594
1600
  {
1595
1601
  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
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1603
+ description: "Insert markdown content into a note immediately after a named section; use to add content under a specific heading without rewriting it. The section ends at the next heading of equal-or-higher level (or end of doc). 404 if no heading matches `headingText` exactly - call `naumu_note_read` first to see the live structure.",
1604
+ inputSchema: z36.object({
1605
+ noteId: z36.string().describe("The note (Thought) ID"),
1606
+ headingText: z36.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1607
+ markdown: z36.string().min(1).describe("Markdown content to insert at the end of that section")
1602
1608
  })
1603
1609
  },
1604
1610
  async ({ noteId, headingText, markdown }) => {
@@ -1614,19 +1620,19 @@ function registerNoteInsert(server2, client2) {
1614
1620
  }
1615
1621
 
1616
1622
  // ../mcp-core/src/tools/note-replace-section.ts
1617
- import { z as z39 } from "zod";
1623
+ import { z as z37 } from "zod";
1618
1624
  function registerNoteReplaceSection(server2, client2) {
1619
1625
  server2.registerTool(
1620
1626
  "naumu_note_replace_section",
1621
1627
  {
1622
1628
  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.")
1629
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1630
+ description: "Replace the body under a named heading with new markdown; use to rewrite one section of a note while leaving the rest intact. By default the heading row itself is preserved (set `keepHeading: false` to drop it too). 404 if no heading matches.",
1631
+ inputSchema: z37.object({
1632
+ noteId: z37.string().describe("The note (Thought) ID"),
1633
+ headingText: z37.string().min(1).describe("Exact text of the heading anchoring the section"),
1634
+ markdown: z37.string().describe("Replacement markdown for the section body"),
1635
+ keepHeading: z37.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1630
1636
  })
1631
1637
  },
1632
1638
  async ({ noteId, headingText, markdown, keepHeading }) => {
@@ -1643,17 +1649,17 @@ function registerNoteReplaceSection(server2, client2) {
1643
1649
  }
1644
1650
 
1645
1651
  // ../mcp-core/src/tools/note-delete-section.ts
1646
- import { z as z40 } from "zod";
1652
+ import { z as z38 } from "zod";
1647
1653
  function registerNoteDeleteSection(server2, client2) {
1648
1654
  server2.registerTool(
1649
1655
  "naumu_note_delete_section",
1650
1656
  {
1651
1657
  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")
1658
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1659
+ description: "\u26A0 DESTRUCTIVE: remove a heading row plus its body (down to the next heading of equal-or-higher level); ONLY use when the user explicitly asks to drop a section. Anything inside that section is gone - there is no per-call undo. If you're unsure which heading they meant, call `naumu_note_read` first to see the current structure. Returns 404 if `headingText` does not exactly match any live heading.",
1660
+ inputSchema: z38.object({
1661
+ noteId: z38.string().describe("The note (Thought) ID"),
1662
+ headingText: z38.string().min(1).describe("Exact text of the heading whose section will be deleted")
1657
1663
  })
1658
1664
  },
1659
1665
  async ({ noteId, headingText }) => {
@@ -1668,17 +1674,17 @@ function registerNoteDeleteSection(server2, client2) {
1668
1674
  }
1669
1675
 
1670
1676
  // ../mcp-core/src/tools/note-replace.ts
1671
- import { z as z41 } from "zod";
1677
+ import { z as z39 } from "zod";
1672
1678
  function registerNoteReplace(server2, client2) {
1673
1679
  server2.registerTool(
1674
1680
  "naumu_note_replace",
1675
1681
  {
1676
1682
  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")
1683
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1684
+ description: "\u26A0 DESTRUCTIVE: replace the entire note content with new markdown; ONLY use when the user explicitly asks to rewrite/replace the whole note. Any concurrent human edits made during the call are silently overwritten. For additive work prefer `naumu_note_append`. For section-level edits use `naumu_note_replace_section`. For inline tweaks use `naumu_note_find_replace`. Read with `naumu_note_read` first if you weren't the last writer.",
1685
+ inputSchema: z39.object({
1686
+ noteId: z39.string().describe("The note (Thought) ID"),
1687
+ markdown: z39.string().describe("New markdown content for the entire note")
1682
1688
  })
1683
1689
  },
1684
1690
  async ({ noteId, markdown }) => {
@@ -1691,19 +1697,19 @@ function registerNoteReplace(server2, client2) {
1691
1697
  }
1692
1698
 
1693
1699
  // ../mcp-core/src/tools/note-find-replace.ts
1694
- import { z as z42 } from "zod";
1700
+ import { z as z40 } from "zod";
1695
1701
  function registerNoteFindReplace(server2, client2) {
1696
1702
  server2.registerTool(
1697
1703
  "naumu_note_find_replace",
1698
1704
  {
1699
1705
  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.")
1706
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1707
+ description: "Literal find/replace within a note's text content; use for mid-paragraph tweaks the section-based tools can't target. Marks (bold, italic, code, etc.) are preserved on the surrounding text. \u26A0 The match is literal-substring across every text leaf in the doc; an overly generic `find` (e.g. \" a \") can rewrite the doc unrecognizably. Pick a phrase distinctive enough to land where you mean. By default replaces every occurrence; set `all: false` for first-only. Returns `{ replacements }` so you can sanity-check the count.",
1708
+ inputSchema: z40.object({
1709
+ noteId: z40.string().describe("The note (Thought) ID"),
1710
+ find: z40.string().min(1).describe("Substring to search for. Literal - no regex."),
1711
+ replace: z40.string().describe("Replacement string. May be empty to delete the match."),
1712
+ all: z40.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1707
1713
  })
1708
1714
  },
1709
1715
  async ({ noteId, find, replace, all }) => {
@@ -1720,7 +1726,7 @@ function registerNoteFindReplace(server2, client2) {
1720
1726
  }
1721
1727
 
1722
1728
  // ../mcp-core/src/tools/canvas-add-element.ts
1723
- import { z as z43 } from "zod";
1729
+ import { z as z41 } from "zod";
1724
1730
  var ELEMENT_TYPES = [
1725
1731
  "rectangle",
1726
1732
  "ellipse",
@@ -1738,25 +1744,25 @@ function registerCanvasAddElement(server2, client2) {
1738
1744
  "naumu_canvas_add_element",
1739
1745
  {
1740
1746
  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.")
1747
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1748
+ description: "Add a new element to a canvas; use to draw a shape, text, or embed onto a drawing surface. Other participants see your colored cursor at the element's center while it lands. The server fills in `id`, `version`, `fractionalIndex`, and `seed` automatically - 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).",
1749
+ inputSchema: z41.object({
1750
+ canvasId: z41.string().describe("The canvas ID"),
1751
+ element: z41.object({
1752
+ type: z41.enum(ELEMENT_TYPES).describe("Element shape"),
1753
+ x: z41.number().describe("Top-left x coordinate in canvas space"),
1754
+ y: z41.number().describe("Top-left y coordinate in canvas space"),
1755
+ width: z41.number().describe("Width in canvas units"),
1756
+ height: z41.number().describe("Height in canvas units"),
1757
+ strokeColor: z41.string().optional().describe("Stroke color (hex). Default #1e1e1e."),
1758
+ fillColor: z41.string().optional().describe('Fill color (hex) or "transparent". Default transparent.'),
1759
+ strokeWidth: z41.number().optional().describe("Stroke width. Default 2."),
1760
+ opacity: z41.number().min(0).max(1).optional().describe("0..1, default 1"),
1761
+ roughness: z41.number().min(0).max(2).optional().describe("Hand-drawn roughness 0..2. Default 1."),
1762
+ label: z41.string().optional().describe("Optional label/text content"),
1763
+ labelFontSize: z41.number().optional().describe("Label font size"),
1764
+ angle: z41.number().optional().describe("Rotation in radians. Default 0.")
1765
+ }).passthrough().describe("Element fields. Pass only what you set - defaults fill the rest.")
1760
1766
  })
1761
1767
  },
1762
1768
  async ({ canvasId, element }) => {
@@ -1769,18 +1775,18 @@ function registerCanvasAddElement(server2, client2) {
1769
1775
  }
1770
1776
 
1771
1777
  // ../mcp-core/src/tools/canvas-update-element.ts
1772
- import { z as z44 } from "zod";
1778
+ import { z as z42 } from "zod";
1773
1779
  function registerCanvasUpdateElement(server2, client2) {
1774
1780
  server2.registerTool(
1775
1781
  "naumu_canvas_update_element",
1776
1782
  {
1777
1783
  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
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1785
+ description: "Patch an existing canvas element; use to move, resize, recolor, or relabel a shape already on the canvas. Only the fields you pass in `changes` are updated - `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`).",
1786
+ inputSchema: z42.object({
1787
+ canvasId: z42.string().describe("The canvas ID"),
1788
+ elementId: z42.string().describe("The element ID returned by `naumu_canvas_add_element` or `naumu_get_canvas_elements`"),
1789
+ changes: z42.record(z42.string(), z42.unknown()).describe("Partial element fields to merge in. Server bumps `version` automatically.")
1784
1790
  })
1785
1791
  },
1786
1792
  async ({ canvasId, elementId, changes }) => {
@@ -1796,17 +1802,17 @@ function registerCanvasUpdateElement(server2, client2) {
1796
1802
  }
1797
1803
 
1798
1804
  // ../mcp-core/src/tools/canvas-remove-element.ts
1799
- import { z as z45 } from "zod";
1805
+ import { z as z43 } from "zod";
1800
1806
  function registerCanvasRemoveElement(server2, client2) {
1801
1807
  server2.registerTool(
1802
1808
  "naumu_canvas_remove_element",
1803
1809
  {
1804
1810
  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")
1811
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1812
+ description: "Soft-delete a canvas element; use to remove a shape from a canvas. The element is tombstoned (isDeleted=true) so concurrent edits don't resurrect it. 404 if the element id is not present on this canvas.",
1813
+ inputSchema: z43.object({
1814
+ canvasId: z43.string().describe("The canvas ID"),
1815
+ elementId: z43.string().describe("The element ID to delete")
1810
1816
  })
1811
1817
  },
1812
1818
  async ({ canvasId, elementId }) => {
@@ -1819,17 +1825,17 @@ function registerCanvasRemoveElement(server2, client2) {
1819
1825
  }
1820
1826
 
1821
1827
  // ../mcp-core/src/tools/create-note.ts
1822
- import { z as z46 } from "zod";
1828
+ import { z as z44 } from "zod";
1823
1829
  function registerCreateNote(server2, client2) {
1824
1830
  server2.registerTool(
1825
1831
  "naumu_create_note",
1826
1832
  {
1827
1833
  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")
1834
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1835
+ description: "Create a new empty note in a graph; use when you need a fresh note to write into. Returns the new note row including its `id` - pass that id to `naumu_note_append` / `naumu_note_replace` to fill in the content. Bots can only create notes in their own graph.",
1836
+ inputSchema: z44.object({
1837
+ graphId: z44.string().describe("The graph ID to create the note in"),
1838
+ title: z44.string().optional().describe("Optional title for the note")
1833
1839
  })
1834
1840
  },
1835
1841
  async ({ graphId, title }) => {
@@ -1842,17 +1848,17 @@ function registerCreateNote(server2, client2) {
1842
1848
  }
1843
1849
 
1844
1850
  // ../mcp-core/src/tools/create-canvas.ts
1845
- import { z as z47 } from "zod";
1851
+ import { z as z45 } from "zod";
1846
1852
  function registerCreateCanvas(server2, client2) {
1847
1853
  server2.registerTool(
1848
1854
  "naumu_create_canvas",
1849
1855
  {
1850
1856
  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")
1857
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1858
+ description: "Create a new empty canvas in a graph; use when you need a fresh drawing surface. Returns the new canvas row including its `id` - pass that id to `naumu_canvas_add_element` to start drawing. Bots can only create canvases in their own graph.",
1859
+ inputSchema: z45.object({
1860
+ graphId: z45.string().describe("The graph ID to create the canvas in"),
1861
+ title: z45.string().optional().describe("Optional title for the canvas")
1856
1862
  })
1857
1863
  },
1858
1864
  async ({ graphId, title }) => {
@@ -1865,42 +1871,101 @@ function registerCreateCanvas(server2, client2) {
1865
1871
  }
1866
1872
 
1867
1873
  // ../mcp-core/src/tools/list-schema-violations.ts
1868
- import { z as z48 } from "zod";
1874
+ import { z as z46 } from "zod";
1875
+ var DEFAULT_EXAMPLE_LIMIT = 5;
1876
+ var rowsForKind = (violations, kind) => {
1877
+ const rows = [];
1878
+ for (const v of violations) {
1879
+ for (const issue of v.issues) {
1880
+ if (issue.kind !== kind) continue;
1881
+ rows.push({ nodeId: v.nodeId, label: v.label, type: v.type, message: issue.message });
1882
+ }
1883
+ }
1884
+ return rows;
1885
+ };
1886
+ var allKinds = (violations) => {
1887
+ const seen = /* @__PURE__ */ new Set();
1888
+ for (const v of violations) {
1889
+ for (const issue of v.issues) seen.add(issue.kind);
1890
+ }
1891
+ return [...seen];
1892
+ };
1869
1893
  function registerListSchemaViolations(server2, client2) {
1870
1894
  server2.registerTool(
1871
1895
  "naumu_list_schema_violations",
1872
1896
  {
1873
1897
  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")
1898
+ annotations: { readOnlyHint: true, openWorldHint: false },
1899
+ description: "Audit a graph against its schema. By default returns a compact summary: total counts plus, for each violation kind, its count and up to 5 example nodes (id/label/type/message) \u2014 small enough not to flood the client. Violation kinds: parent_missing (schema expects a parent edge that does not exist), parent_multiple (more than one parent edge where one is expected), parent_mismatch (parent edge has wrong target type or relation label), parent_not_backbone (an edge uses a backbone/parent relation but is not stored as a backbone edge, so the subtree stays off the hierarchy), unknown_relation (edge uses a relation not in the schema), invalid_connection_target (edge connects to a type the schema does not allow for this source), unknown_type (node carries a type no longer in the schema), disconnected (node heads a group with no backbone path to the main tree). To see every node for one kind, pass `kind` to filter; `limit` caps how many rows are returned (examples in the default summary, or full rows when `kind` is set). Use for audits, import-verification, and CI-style checks after batch writes.",
1900
+ inputSchema: z46.object({
1901
+ graphId: z46.string().describe("The graph ID"),
1902
+ kind: z46.string().optional().describe(
1903
+ 'Drill into one violation kind (e.g. "parent_not_backbone"). Returns the full list of nodes with that kind, up to `limit`, instead of the summary.'
1904
+ ),
1905
+ limit: z46.number().int().min(1).optional().describe(
1906
+ "Max rows to return. When `kind` is set, caps the full drill-down list (default: all). Otherwise caps example nodes per kind in the summary (default: 5)."
1907
+ )
1878
1908
  })
1879
1909
  },
1880
- async ({ graphId }) => {
1910
+ async ({ graphId, kind, limit }) => {
1881
1911
  const data = await client2.get(
1882
1912
  `/api/graphs/${graphId}/schema/validation`
1883
1913
  );
1914
+ const { violations, summary } = data;
1915
+ if (kind) {
1916
+ const rows = rowsForKind(violations, kind);
1917
+ const capped = limit !== void 0 ? rows.slice(0, limit) : rows;
1918
+ const payload2 = {
1919
+ graphId,
1920
+ kind,
1921
+ nodeCount: summary.nodeCount,
1922
+ totalForKind: rows.length,
1923
+ returned: capped.length,
1924
+ truncated: capped.length < rows.length,
1925
+ rows: capped
1926
+ };
1927
+ return {
1928
+ content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }]
1929
+ };
1930
+ }
1931
+ const exampleLimit = limit ?? DEFAULT_EXAMPLE_LIMIT;
1932
+ const byKind = allKinds(violations).map((k) => {
1933
+ const rows = rowsForKind(violations, k);
1934
+ return {
1935
+ kind: k,
1936
+ count: summary.byKind[k] ?? rows.length,
1937
+ examples: rows.slice(0, exampleLimit)
1938
+ };
1939
+ }).sort((a, b) => b.count - a.count);
1940
+ const payload = {
1941
+ graphId,
1942
+ nodeCount: summary.nodeCount,
1943
+ nodesWithIssues: summary.nodesWithIssues,
1944
+ totalIssues: summary.totalIssues,
1945
+ exampleLimit,
1946
+ byKind,
1947
+ hint: summary.totalIssues === 0 ? "No violations \u2014 the graph conforms to its schema." : "Pass `kind` to list every node for one violation kind (use `limit` to cap rows)."
1948
+ };
1884
1949
  return {
1885
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1950
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1886
1951
  };
1887
1952
  }
1888
1953
  );
1889
1954
  }
1890
1955
 
1891
1956
  // ../mcp-core/src/tools/list-dense-nodes.ts
1892
- import { z as z49 } from "zod";
1957
+ import { z as z47 } from "zod";
1893
1958
  function registerListDenseNodes(server2, client2) {
1894
1959
  server2.registerTool(
1895
1960
  "naumu_list_dense_nodes",
1896
1961
  {
1897
1962
  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.")
1963
+ annotations: { readOnlyHint: true, openWorldHint: false },
1964
+ description: 'Return nodes whose total edge count (in+out, non-system) is \u2265 minConnections, grouped by type; use for /restructure hub detection. Each row includes `same_typed_child_count` - the number of children of the SAME type as the node (the Naumu hub-pattern signal). 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 - runs a single Cypher aggregation.',
1965
+ inputSchema: z47.object({
1966
+ graphId: z47.string().describe("The graph ID"),
1967
+ minConnections: z47.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."),
1968
+ nodeTypes: z47.array(z47.string()).optional().describe("Optional list of node types to restrict the scan to.")
1904
1969
  })
1905
1970
  },
1906
1971
  async ({ graphId, minConnections, nodeTypes }) => {
@@ -1918,19 +1983,19 @@ function registerListDenseNodes(server2, client2) {
1918
1983
  }
1919
1984
 
1920
1985
  // ../mcp-core/src/tools/list-node-connections.ts
1921
- import { z as z50 } from "zod";
1986
+ import { z as z48 } from "zod";
1922
1987
  function registerListNodeConnections(server2, client2) {
1923
1988
  server2.registerTool(
1924
1989
  "naumu_list_node_connections",
1925
1990
  {
1926
1991
  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).')
1992
+ annotations: { readOnlyHint: true, openWorldHint: false },
1993
+ description: 'Return a single node\'s edges (non-system) with the connected node on the other side; use during /restructure to confirm mini-hub candidates and verify reparenting outcomes. Filter with `edgeType` (relation label) and `direction` ("in" | "out" | "both", default both). Response: `{ node: {id,label,type}, edges: [{relation, direction, isParent, other: {id,label,type}}] }`.',
1994
+ inputSchema: z48.object({
1995
+ graphId: z48.string().describe("The graph ID"),
1996
+ nodeId: z48.string().describe("The node ID to inspect"),
1997
+ edgeType: z48.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
1998
+ direction: z48.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
1934
1999
  })
1935
2000
  },
1936
2001
  async ({ graphId, nodeId, edgeType, direction }) => {
@@ -1948,19 +2013,19 @@ function registerListNodeConnections(server2, client2) {
1948
2013
  }
1949
2014
 
1950
2015
  // ../mcp-core/src/tools/reparent.ts
1951
- import { z as z51 } from "zod";
2016
+ import { z as z49 } from "zod";
1952
2017
  function registerReparent(server2, client2) {
1953
2018
  server2.registerTool(
1954
2019
  "naumu_reparent",
1955
2020
  {
1956
2021
  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).')
2022
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2023
+ description: 'Atomically swap a node\'s parent edge; use to move a child under a different parent (e.g. during /restructure to reparent children under newly-created mini-hubs). Deletes any existing `isParent: true` edges on the node and creates a new one to `newParentId` with relation `newRelation`. Preserves the node\'s id, content, attributes, and embedding - does NOT trigger embedding regeneration because only the parent edge changes. Idempotent: if the node already has the requested parent edge, response is `status: "skipped"`. Response shape: `{nodeId, oldParentId, newParentId, newRelation, status: "moved" | "skipped"}`.',
2024
+ inputSchema: z49.object({
2025
+ graphId: z49.string().describe("The graph ID"),
2026
+ nodeId: z49.string().describe("The child node to reparent"),
2027
+ newParentId: z49.string().describe("The new parent node id"),
2028
+ newRelation: z49.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
2029
  })
1965
2030
  },
1966
2031
  async ({ graphId, nodeId, newParentId, newRelation }) => {
@@ -1976,19 +2041,19 @@ function registerReparent(server2, client2) {
1976
2041
  }
1977
2042
 
1978
2043
  // ../mcp-core/src/tools/batch-reparent.ts
1979
- import { z as z52 } from "zod";
2044
+ import { z as z50 } from "zod";
1980
2045
  function registerBatchReparent(server2, client2) {
1981
2046
  server2.registerTool(
1982
2047
  "naumu_batch_reparent",
1983
2048
  {
1984
2049
  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`")
2050
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2051
+ description: 'Reparent 1-25 nodes onto a shared `newParentId` with the same `newRelation`; use to move a same-typed cluster under a freshly-created mini-hub in /restructure. Same semantics as `naumu_reparent` per-node: atomic swap of the isParent edge, preserves id/content/attributes/embedding, no re-embedding. Idempotent per node (already-parented nodes return `status: "skipped"`). Per-node response array: `[{nodeId, oldParentId, newParentId, status: "moved" | "skipped" | "error", error?}]`.',
2052
+ inputSchema: z50.object({
2053
+ graphId: z50.string().describe("The graph ID"),
2054
+ newParentId: z50.string().describe("Parent node id every nodeId in the batch will be parented to"),
2055
+ newRelation: z50.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
2056
+ nodeIds: z50.array(z50.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
1992
2057
  })
1993
2058
  },
1994
2059
  async ({ graphId, newParentId, newRelation, nodeIds }) => {
@@ -2005,7 +2070,7 @@ function registerBatchReparent(server2, client2) {
2005
2070
  }
2006
2071
 
2007
2072
  // ../mcp-core/src/tools/chatgpt-search.ts
2008
- import { z as z53 } from "zod";
2073
+ import { z as z51 } from "zod";
2009
2074
 
2010
2075
  // ../mcp-core/src/public-origin.ts
2011
2076
  function publicOrigin() {
@@ -2057,10 +2122,10 @@ function registerChatgptSearch(server2, client2) {
2057
2122
  "search",
2058
2123
  {
2059
2124
  title: "Search",
2060
- annotations: { readOnlyHint: true },
2125
+ annotations: { readOnlyHint: true, openWorldHint: false },
2061
2126
  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").')
2127
+ inputSchema: z51.object({
2128
+ query: z51.string().describe('Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa").')
2064
2129
  })
2065
2130
  },
2066
2131
  async ({ query }) => {
@@ -2092,7 +2157,7 @@ function registerChatgptSearch(server2, client2) {
2092
2157
  }
2093
2158
 
2094
2159
  // ../mcp-core/src/tools/chatgpt-fetch.ts
2095
- import { z as z54 } from "zod";
2160
+ import { z as z52 } from "zod";
2096
2161
  var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2097
2162
  "id",
2098
2163
  "label",
@@ -2155,10 +2220,10 @@ function registerChatgptFetch(server2, client2) {
2155
2220
  "fetch",
2156
2221
  {
2157
2222
  title: "Fetch",
2158
- annotations: { readOnlyHint: true },
2223
+ annotations: { readOnlyHint: true, openWorldHint: false },
2159
2224
  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>`.")
2225
+ inputSchema: z52.object({
2226
+ id: z52.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2162
2227
  })
2163
2228
  },
2164
2229
  async ({ id }) => {
@@ -2221,14 +2286,13 @@ var TOOL_REGISTRARS = {
2221
2286
  naumu_remove_edge: registerRemoveEdge,
2222
2287
  naumu_remove_edges_bulk: registerRemoveEdgesBulk,
2223
2288
  naumu_ask: registerAsk,
2224
- naumu_get_ai_thread: registerGetAiThread,
2225
- naumu_traverse: registerTraverse,
2289
+ naumu_delegate: registerDelegate,
2290
+ // naumu_traverse omitted on purpose — backend stub returns 503 (see import note).
2226
2291
  naumu_list_canvases: registerListCanvases,
2227
2292
  naumu_get_canvas_elements: registerGetCanvasElements,
2228
2293
  naumu_get_canvas_image: registerGetCanvasImage,
2229
2294
  naumu_post_message: registerPostMessage,
2230
2295
  naumu_read_thread: registerReadThread,
2231
- naumu_ask_naumu: registerAskNaumu,
2232
2296
  naumu_whoami: registerWhoami,
2233
2297
  naumu_list_threads: registerListThreads,
2234
2298
  naumu_get_thread: registerGetThread,