@naumu/mcp 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +483 -301
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -153,8 +153,44 @@ function registerListGraphs(server2, client2) {
153
153
  );
154
154
  }
155
155
 
156
- // ../mcp-core/src/tools/create-graph.ts
156
+ // ../mcp-core/src/tools/list-members.ts
157
157
  import { z as z2 } from "zod";
158
+ function registerListMembers(server2, client2) {
159
+ server2.registerTool(
160
+ "naumu_list_members",
161
+ {
162
+ title: "List Members",
163
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
164
+ description: "List the members of a Naumu space (graph): each member's userId, name, email, and role. Use it to discover teammates and to get the id you pass when @mentioning a human via naumu_post_message (their userId or email both work).",
165
+ inputSchema: z2.object({
166
+ graphId: z2.string().describe("The space (graph) ID to list members of. You must be a member of this space.")
167
+ })
168
+ },
169
+ async ({ graphId }) => {
170
+ try {
171
+ const data = await client2.get(`/api/graphs/${graphId}/members`);
172
+ const members = Array.isArray(data) ? data.map((m) => ({
173
+ userId: m.userId,
174
+ name: m.name,
175
+ email: m.email,
176
+ role: m.role
177
+ })) : data;
178
+ return {
179
+ content: [{ type: "text", text: JSON.stringify(members, null, 2) }]
180
+ };
181
+ } catch (err) {
182
+ const message = err instanceof Error ? err.message : String(err);
183
+ return {
184
+ content: [{ type: "text", text: `Error: ${message}` }],
185
+ isError: true
186
+ };
187
+ }
188
+ }
189
+ );
190
+ }
191
+
192
+ // ../mcp-core/src/tools/create-graph.ts
193
+ import { z as z3 } from "zod";
158
194
  function registerCreateGraph(server2, client2) {
159
195
  server2.registerTool(
160
196
  "naumu_create_graph",
@@ -162,8 +198,8 @@ function registerCreateGraph(server2, client2) {
162
198
  title: "Create Graph",
163
199
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
164
200
  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.",
165
- inputSchema: z2.object({
166
- name: z2.string().min(1).describe("Display name for the new space. A URL slug is generated from this name.")
201
+ inputSchema: z3.object({
202
+ name: z3.string().min(1).describe("Display name for the new space. A URL slug is generated from this name.")
167
203
  })
168
204
  },
169
205
  async ({ name }) => {
@@ -176,7 +212,7 @@ function registerCreateGraph(server2, client2) {
176
212
  }
177
213
 
178
214
  // ../mcp-core/src/tools/get-schema.ts
179
- import { z as z3 } from "zod";
215
+ import { z as z4 } from "zod";
180
216
  function formatConnection(c) {
181
217
  const out = { relation: c.relation };
182
218
  if (c.polymorphic) {
@@ -224,8 +260,8 @@ function registerGetSchema(server2, client2) {
224
260
  title: "Get Graph Schema",
225
261
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
226
262
  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.",
227
- inputSchema: z3.object({
228
- graphId: z3.string().describe("The graph ID")
263
+ inputSchema: z4.object({
264
+ graphId: z4.string().describe("The graph ID")
229
265
  })
230
266
  },
231
267
  async ({ graphId }) => {
@@ -247,40 +283,40 @@ function registerGetSchema(server2, client2) {
247
283
  }
248
284
 
249
285
  // ../mcp-core/src/tools/update-schema.ts
250
- import { z as z4 } from "zod";
251
- var ConnectionSchema = z4.object({
252
- relation: z4.string().describe("UPPER_SNAKE_CASE relation name (e.g. WORKS_AT, BUILT_BY, BELONGS_TO)"),
253
- target_node: z4.string().optional().describe("Target type name. Omit when polymorphic=true."),
254
- polymorphic: z4.boolean().optional().describe("Set true when this relation can target multiple types.")
286
+ import { z as z5 } from "zod";
287
+ var ConnectionSchema = z5.object({
288
+ relation: z5.string().describe("UPPER_SNAKE_CASE relation name (e.g. WORKS_AT, BUILT_BY, BELONGS_TO)"),
289
+ target_node: z5.string().optional().describe("Target type name. Omit when polymorphic=true."),
290
+ polymorphic: z5.boolean().optional().describe("Set true when this relation can target multiple types.")
255
291
  });
256
- var AttributeValueSchema = z4.object({
257
- label: z4.string().describe("Display label for this enum value"),
258
- color: z4.string().optional().describe('Optional hex color (e.g. "#ff5722")'),
259
- description: z4.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won - signed and revenue committed"). Encouraged when the label alone is ambiguous.')
292
+ var AttributeValueSchema = z5.object({
293
+ label: z5.string().describe("Display label for this enum value"),
294
+ color: z5.string().optional().describe('Optional hex color (e.g. "#ff5722")'),
295
+ description: z5.string().optional().describe('Short note distinguishing this value from sibling values (e.g. "closed-won - signed and revenue committed"). Encouraged when the label alone is ambiguous.')
260
296
  });
261
- var AttributeSchema = z4.object({
262
- name: z4.string().describe('Attribute key (e.g. "stage", "status", "category")'),
263
- type: z4.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories with values; string/number/date hold free-form per-node data with values []. Calendar dates use "date", not select values.'),
264
- values: z4.array(AttributeValueSchema).describe("Allowed enum values for select/multiselect; pass [] for string/number/date."),
265
- description: z4.string().optional().describe("Short note explaining what this attribute captures and how it differs from similarly-named attributes elsewhere in the schema. Strongly encouraged.")
297
+ var AttributeSchema = z5.object({
298
+ name: z5.string().describe('Attribute key (e.g. "stage", "status", "category")'),
299
+ type: z5.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories with values; string/number/date hold free-form per-node data with values []. Calendar dates use "date", not select values.'),
300
+ values: z5.array(AttributeValueSchema).describe("Allowed enum values for select/multiselect; pass [] for string/number/date."),
301
+ description: z5.string().optional().describe("Short note explaining what this attribute captures and how it differs from similarly-named attributes elsewhere in the schema. Strongly encouraged.")
266
302
  });
267
- var NodeTypeSchema = z4.object({
268
- type: z4.string().describe("Type name in PascalCase (e.g. TypeA, TypeB)"),
269
- connections: z4.object({
303
+ var NodeTypeSchema = z5.object({
304
+ type: z5.string().describe("Type name in PascalCase (e.g. TypeA, TypeB)"),
305
+ connections: z5.object({
270
306
  parent: ConnectionSchema.optional().describe("Optional parent relation (this type nests under another via this connection)."),
271
- required: z4.array(ConnectionSchema).default([]).describe("Required outgoing connections to other types."),
272
- suggested: z4.array(ConnectionSchema).default([]).describe("Suggested-but-optional outgoing connections.")
307
+ required: z5.array(ConnectionSchema).default([]).describe("Required outgoing connections to other types."),
308
+ suggested: z5.array(ConnectionSchema).default([]).describe("Suggested-but-optional outgoing connections.")
273
309
  }),
274
- attributes: z4.array(AttributeSchema).optional().describe("Type-level attributes for instances of this type."),
275
- defaultVisibility: z4.enum(["restricted", "internal", "open"]).optional().describe(
310
+ attributes: z5.array(AttributeSchema).optional().describe("Type-level attributes for instances of this type."),
311
+ defaultVisibility: z5.enum(["restricted", "internal", "open"]).optional().describe(
276
312
  '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.'
277
313
  ),
278
- color: z4.string().optional().describe("Optional hex color for instances of this type."),
279
- description: z4.string().optional().describe('Short one-sentence description of what this type represents AND how it differs from semantically similar types (e.g. "Type A - distinct from Type B, which is broader"). Strongly encouraged on every type. Future agents rely on this when classifying a new node into one of several similar-sounding types.')
314
+ color: z5.string().optional().describe("Optional hex color for instances of this type."),
315
+ description: z5.string().optional().describe('Short one-sentence description of what this type represents AND how it differs from semantically similar types (e.g. "Type A - distinct from Type B, which is broader"). Strongly encouraged on every type. Future agents rely on this when classifying a new node into one of several similar-sounding types.')
280
316
  });
281
- var SchemaDefinitionSchema = z4.object({
282
- description: z4.string().optional().describe("Schema-level description / domain summary."),
283
- nodes: z4.array(NodeTypeSchema).describe("All node types in the schema.")
317
+ var SchemaDefinitionSchema = z5.object({
318
+ description: z5.string().optional().describe("Schema-level description / domain summary."),
319
+ nodes: z5.array(NodeTypeSchema).describe("All node types in the schema.")
284
320
  });
285
321
  function registerUpdateSchema(server2, client2) {
286
322
  server2.registerTool(
@@ -289,8 +325,8 @@ function registerUpdateSchema(server2, client2) {
289
325
  title: "Update Graph Schema",
290
326
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
291
327
  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, relation names UPPER_SNAKE_CASE. Bias toward general types - refine via attributes or nested children, not type proliferation.",
292
- inputSchema: z4.object({
293
- graphId: z4.string().describe("The graph ID"),
328
+ inputSchema: z5.object({
329
+ graphId: z5.string().describe("The graph ID"),
294
330
  schema: SchemaDefinitionSchema
295
331
  })
296
332
  },
@@ -306,22 +342,22 @@ function registerUpdateSchema(server2, client2) {
306
342
  }
307
343
 
308
344
  // ../mcp-core/src/tools/add-node-type.ts
309
- import { z as z5 } from "zod";
310
- var ConnectionSchema2 = z5.object({
311
- relation: z5.string().describe("UPPER_SNAKE_CASE relation name (WORKS_AT, BELONGS_TO)."),
312
- target_node: z5.string().optional().describe("Target type name. Omit when polymorphic=true."),
313
- polymorphic: z5.boolean().optional().describe("True when this relation can target many types.")
345
+ import { z as z6 } from "zod";
346
+ var ConnectionSchema2 = z6.object({
347
+ relation: z6.string().describe("UPPER_SNAKE_CASE relation name (WORKS_AT, BELONGS_TO)."),
348
+ target_node: z6.string().optional().describe("Target type name. Omit when polymorphic=true."),
349
+ polymorphic: z6.boolean().optional().describe("True when this relation can target many types.")
314
350
  });
315
- var AttributeValueSchema2 = z5.object({
316
- label: z5.string(),
317
- color: z5.string().optional(),
318
- description: z5.string().optional().describe('Short note distinguishing this value from sibling values (e.g. for status="closed-won", "signed and revenue committed"). Encouraged when the label alone is ambiguous.')
351
+ var AttributeValueSchema2 = z6.object({
352
+ label: z6.string(),
353
+ color: z6.string().optional(),
354
+ description: z6.string().optional().describe('Short note distinguishing this value from sibling values (e.g. for status="closed-won", "signed and revenue committed"). Encouraged when the label alone is ambiguous.')
319
355
  });
320
- var AttributeSchema2 = z5.object({
321
- name: z5.string(),
322
- type: z5.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories with values; string/number/date hold free-form per-node data with values []. Calendar dates use "date", not select values.'),
323
- values: z5.array(AttributeValueSchema2).default([]),
324
- description: z5.string().optional().describe('Short note explaining what this attribute captures and how it differs from similarly-named attributes on other types (e.g. "stage on Type A" vs "stage on Type B"). Strongly encouraged.')
356
+ var AttributeSchema2 = z6.object({
357
+ name: z6.string(),
358
+ type: z6.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories with values; string/number/date hold free-form per-node data with values []. Calendar dates use "date", not select values.'),
359
+ values: z6.array(AttributeValueSchema2).default([]),
360
+ description: z6.string().optional().describe('Short note explaining what this attribute captures and how it differs from similarly-named attributes on other types (e.g. "stage on Type A" vs "stage on Type B"). Strongly encouraged.')
325
361
  });
326
362
  function registerAddNodeType(server2, client2) {
327
363
  server2.registerTool(
@@ -330,18 +366,18 @@ function registerAddNodeType(server2, client2) {
330
366
  title: "Add Node Type to Schema",
331
367
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
332
368
  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. Relation names UPPER_SNAKE_CASE. (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. "Type A \u2014 distinct from Type B, which is broader"). 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.',
333
- inputSchema: z5.object({
334
- graphId: z5.string(),
335
- type: z5.string().describe("PascalCase type name"),
336
- description: z5.string().optional().describe(
369
+ inputSchema: z6.object({
370
+ graphId: z6.string(),
371
+ type: z6.string().describe("PascalCase type name"),
372
+ description: z6.string().optional().describe(
337
373
  "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."
338
374
  ),
339
375
  parent: ConnectionSchema2.optional().describe("Optional parent connection - sets this type as a child of another type."),
340
- required: z5.array(ConnectionSchema2).optional(),
341
- suggested: z5.array(ConnectionSchema2).optional(),
342
- attributes: z5.array(AttributeSchema2).optional(),
343
- color: z5.string().optional(),
344
- defaultVisibility: z5.enum(["restricted", "internal", "open"]).optional().describe(
376
+ required: z6.array(ConnectionSchema2).optional(),
377
+ suggested: z6.array(ConnectionSchema2).optional(),
378
+ attributes: z6.array(AttributeSchema2).optional(),
379
+ color: z6.string().optional(),
380
+ defaultVisibility: z6.enum(["restricted", "internal", "open"]).optional().describe(
345
381
  'Default visibility for NEW nodes of this type when no explicit visibility is passed on creation. Does NOT retroactively change existing nodes. "open" = visible to anyone with the space link, "internal" = visible to all space members, "restricted" = only members explicitly granted access. Omit to fall back to the space-level defaultVisibility.'
346
382
  )
347
383
  })
@@ -376,7 +412,7 @@ function registerAddNodeType(server2, client2) {
376
412
  }
377
413
 
378
414
  // ../mcp-core/src/tools/add-connection.ts
379
- import { z as z6 } from "zod";
415
+ import { z as z7 } from "zod";
380
416
  function registerAddConnection(server2, client2) {
381
417
  server2.registerTool(
382
418
  "naumu_add_connection",
@@ -384,13 +420,13 @@ function registerAddConnection(server2, client2) {
384
420
  title: "Add Connection to Node Type",
385
421
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
386
422
  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. Type A under Type A) does NOT need a new connection - use the existing parent relation. Relation name UPPER_SNAKE_CASE.',
387
- inputSchema: z6.object({
388
- graphId: z6.string(),
389
- source_type: z6.string().describe("Existing node type to add the connection to."),
390
- relation: z6.string().describe("UPPER_SNAKE_CASE relation name."),
391
- target_node: z6.string().optional().describe("Target type. Omit when polymorphic=true."),
392
- polymorphic: z6.boolean().optional(),
393
- kind: z6.enum(["required", "suggested", "parent"]).default("suggested")
423
+ inputSchema: z7.object({
424
+ graphId: z7.string(),
425
+ source_type: z7.string().describe("Existing node type to add the connection to."),
426
+ relation: z7.string().describe("UPPER_SNAKE_CASE relation name."),
427
+ target_node: z7.string().optional().describe("Target type. Omit when polymorphic=true."),
428
+ polymorphic: z7.boolean().optional(),
429
+ kind: z7.enum(["required", "suggested", "parent"]).default("suggested")
394
430
  })
395
431
  },
396
432
  async ({ graphId, source_type, relation, target_node, polymorphic, kind }) => {
@@ -426,7 +462,7 @@ function registerAddConnection(server2, client2) {
426
462
  }
427
463
 
428
464
  // ../mcp-core/src/tools/add-attribute.ts
429
- import { z as z7 } from "zod";
465
+ import { z as z8 } from "zod";
430
466
  function registerAddAttribute(server2, client2) {
431
467
  server2.registerTool(
432
468
  "naumu_add_attribute",
@@ -434,19 +470,19 @@ function registerAddAttribute(server2, client2) {
434
470
  title: "Add Attribute to Node Type",
435
471
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
436
472
  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). 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.',
437
- inputSchema: z7.object({
438
- graphId: z7.string(),
439
- node_type: z7.string().describe("Existing node type to add the attribute to."),
440
- name: z7.string().describe('Attribute key (e.g. "stage", "status").'),
441
- type: z7.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories - provide their values. string/number/date hold free-form per-node data - pass values: []. A specific calendar date is per-node data, so use type "date" and write the date on the node, rather than adding the date as a select value.'),
442
- values: z7.array(
443
- z7.object({
444
- label: z7.string(),
445
- color: z7.string().optional(),
446
- 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.')
473
+ inputSchema: z8.object({
474
+ graphId: z8.string(),
475
+ node_type: z8.string().describe("Existing node type to add the attribute to."),
476
+ name: z8.string().describe('Attribute key (e.g. "stage", "status").'),
477
+ type: z8.enum(["select", "multiselect", "string", "number", "date"]).describe('Attribute type (required). select/multiselect define a closed set of categories - provide their values. string/number/date hold free-form per-node data - pass values: []. A specific calendar date is per-node data, so use type "date" and write the date on the node, rather than adding the date as a select value.'),
478
+ values: z8.array(
479
+ z8.object({
480
+ label: z8.string(),
481
+ color: z8.string().optional(),
482
+ description: z8.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.')
447
483
  })
448
484
  ).default([]),
449
- 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.')
485
+ description: z8.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.')
450
486
  })
451
487
  },
452
488
  async ({ graphId, node_type, name, type, values, description }) => {
@@ -480,7 +516,7 @@ function registerAddAttribute(server2, client2) {
480
516
  }
481
517
 
482
518
  // ../mcp-core/src/tools/search.ts
483
- import { z as z8 } from "zod";
519
+ import { z as z9 } from "zod";
484
520
  function registerSearch(server2, client2) {
485
521
  server2.registerTool(
486
522
  "naumu_search",
@@ -488,13 +524,13 @@ function registerSearch(server2, client2) {
488
524
  title: "Search Graph",
489
525
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
490
526
  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.',
491
- inputSchema: z8.object({
492
- graphId: z8.string().describe("The graph ID"),
493
- query: z8.string().describe(
527
+ inputSchema: z9.object({
528
+ graphId: z9.string().describe("The graph ID"),
529
+ query: z9.string().describe(
494
530
  'Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa Twitter handle").'
495
531
  ),
496
- limit: z8.number().optional().default(20).describe("Max results to return (default 20, max 200). Adaptive cutoff may return fewer when the top match is weak."),
497
- nodeTypes: z8.array(z8.string()).optional().describe('Filter to specific node types (e.g. ["Type A", "Type B"])')
532
+ limit: z9.number().optional().default(20).describe("Max results to return (default 20, max 200). Adaptive cutoff may return fewer when the top match is weak."),
533
+ nodeTypes: z9.array(z9.string()).optional().describe('Filter to specific node types (e.g. ["Type A", "Type B"])')
498
534
  })
499
535
  },
500
536
  async ({ graphId, query, limit, nodeTypes }) => {
@@ -511,7 +547,7 @@ function registerSearch(server2, client2) {
511
547
  }
512
548
 
513
549
  // ../mcp-core/src/tools/filter.ts
514
- import { z as z9 } from "zod";
550
+ import { z as z10 } from "zod";
515
551
  function registerFilter(server2, client2) {
516
552
  server2.registerTool(
517
553
  "naumu_filter",
@@ -519,17 +555,17 @@ function registerFilter(server2, client2) {
519
555
  title: "Filter Graph Nodes",
520
556
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
521
557
  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.',
522
- inputSchema: z9.object({
523
- graphId: z9.string().describe("The graph ID"),
524
- nodeTypes: z9.array(z9.string()).optional().describe('Filter to specific node types (e.g. ["Type A", "Type B"])'),
525
- includeAttributes: z9.record(z9.string(), z9.array(z9.string())).optional().describe(
558
+ inputSchema: z10.object({
559
+ graphId: z10.string().describe("The graph ID"),
560
+ nodeTypes: z10.array(z10.string()).optional().describe('Filter to specific node types (e.g. ["Type A", "Type B"])'),
561
+ includeAttributes: z10.record(z10.string(), z10.array(z10.string())).optional().describe(
526
562
  'Only include nodes where attribute matches one of the values. Example: {"Status": ["Todo", "In Progress"]}'
527
563
  ),
528
- excludeAttributes: z9.record(z9.string(), z9.array(z9.string())).optional().describe(
564
+ excludeAttributes: z10.record(z10.string(), z10.array(z10.string())).optional().describe(
529
565
  'Exclude nodes where attribute matches any of the values. Example: {"Status": ["Done", "Wont do"]}'
530
566
  ),
531
- sortBy: z9.enum(["sortKey", "updatedAt", "label"]).optional().default("sortKey").describe('Sort order: "sortKey" (default), "updatedAt" (most recent first), or "label" (alphabetical)'),
532
- limit: z9.number().optional().default(50).describe("Max results to return (default 50, max 200)")
567
+ sortBy: z10.enum(["sortKey", "updatedAt", "label"]).optional().default("sortKey").describe('Sort order: "sortKey" (default), "updatedAt" (most recent first), or "label" (alphabetical)'),
568
+ limit: z10.number().optional().default(50).describe("Max results to return (default 50, max 200)")
533
569
  })
534
570
  },
535
571
  async ({ graphId, nodeTypes, includeAttributes, excludeAttributes, sortBy, limit }) => {
@@ -554,7 +590,7 @@ function registerFilter(server2, client2) {
554
590
  }
555
591
 
556
592
  // ../mcp-core/src/tools/get-node.ts
557
- import { z as z10 } from "zod";
593
+ import { z as z11 } from "zod";
558
594
  function registerGetNode(server2, client2) {
559
595
  server2.registerTool(
560
596
  "naumu_get_node",
@@ -562,9 +598,9 @@ function registerGetNode(server2, client2) {
562
598
  title: "Get Node",
563
599
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
564
600
  description: "Get a single node with all its properties and connections (incoming and outgoing edges).",
565
- inputSchema: z10.object({
566
- graphId: z10.string().describe("The graph ID"),
567
- nodeId: z10.string().describe("The node ID")
601
+ inputSchema: z11.object({
602
+ graphId: z11.string().describe("The graph ID"),
603
+ nodeId: z11.string().describe("The node ID")
568
604
  })
569
605
  },
570
606
  async ({ graphId, nodeId }) => {
@@ -577,38 +613,7 @@ function registerGetNode(server2, client2) {
577
613
  }
578
614
 
579
615
  // ../mcp-core/src/tools/add-node.ts
580
- import { z as z11 } from "zod";
581
- var NodeInput = z11.object({
582
- label: z11.string().describe("Display name of the node"),
583
- type: z11.string().describe("Node type from the graph schema"),
584
- content: z11.string().min(1).describe("Rich text content / description. REQUIRED - every node must explain what it is."),
585
- attributes: z11.record(z11.string(), z11.unknown()).optional().describe("Additional key-value attributes")
586
- });
587
- function registerAddNode(server2, client2) {
588
- server2.registerTool(
589
- "naumu_add_node",
590
- {
591
- title: "Add Nodes (bulk)",
592
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
593
- 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.',
594
- inputSchema: z11.object({
595
- graphId: z11.string().describe("The graph ID"),
596
- nodes: z11.array(NodeInput).min(1).max(25).describe("Batch of 1\u201325 nodes to create. Keep batches small for atomicity.")
597
- })
598
- },
599
- async ({ graphId, nodes }) => {
600
- const payload = nodes.map(({ label, type, content, attributes }) => {
601
- const node = { label, type, content };
602
- if (attributes) Object.assign(node, attributes);
603
- return node;
604
- });
605
- const data = await client2.post(`/api/graphs/${graphId}/nodes`, { nodes: payload });
606
- return {
607
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
608
- };
609
- }
610
- );
611
- }
616
+ import { z as z13 } from "zod";
612
617
 
613
618
  // ../mcp-core/src/tools/update-node.ts
614
619
  import { z as z12 } from "zod";
@@ -740,13 +745,120 @@ Use naumu_get_schema to check valid attributes and values for this node type.`
740
745
  );
741
746
  }
742
747
 
748
+ // ../mcp-core/src/tools/add-node.ts
749
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["id", "label", "type", "content"]);
750
+ function normalizeNodeAttributes(schema, nodeType, attributes) {
751
+ const attrMap = /* @__PURE__ */ new Map();
752
+ const nodeDef = schema.nodes.find((n) => n.type === nodeType);
753
+ for (const attr of nodeDef?.attributes ?? []) {
754
+ attrMap.set(attr.name.toLowerCase(), attr);
755
+ }
756
+ const normalized = {};
757
+ const errors = [];
758
+ for (const [key, val] of Object.entries(attributes)) {
759
+ const lower = key.toLowerCase();
760
+ if (RESERVED_KEYS.has(lower)) {
761
+ errors.push(`Attribute "${key}" conflicts with a reserved node field.`);
762
+ continue;
763
+ }
764
+ const schemaDef = attrMap.get(lower);
765
+ if (!schemaDef) {
766
+ const validNames = [...attrMap.values()].map((a) => a.name);
767
+ errors.push(
768
+ `Unknown attribute "${key}" for type "${nodeType}". Valid attributes: ${validNames.length > 0 ? validNames.join(", ") : "none"}.`
769
+ );
770
+ continue;
771
+ }
772
+ if (schemaDef.type === "date") {
773
+ const result = validateDateAttribute(schemaDef.name, val);
774
+ if (!result.ok) {
775
+ errors.push(result.error);
776
+ continue;
777
+ }
778
+ if (result.value === null) continue;
779
+ const range = typeof result.value === "string" ? { start: result.value } : result.value;
780
+ normalized[lower] = JSON.stringify(range);
781
+ continue;
782
+ }
783
+ const validValues = schemaDef.values.map((v) => v.label);
784
+ if (typeof val === "string" && validValues.length > 0 && !validValues.includes(val)) {
785
+ errors.push(
786
+ `Invalid value "${val}" for attribute "${schemaDef.name}" on type "${nodeType}". Valid values: ${validValues.join(", ")}.`
787
+ );
788
+ continue;
789
+ }
790
+ normalized[lower] = val;
791
+ }
792
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, attributes: normalized };
793
+ }
794
+ var NodeInput = z13.object({
795
+ label: z13.string().describe("Display name of the node"),
796
+ type: z13.string().describe("Node type from the graph schema"),
797
+ content: z13.string().min(1).describe("Rich text content / description. REQUIRED - every node must explain what it is."),
798
+ attributes: z13.record(z13.string(), z13.unknown()).optional().describe(
799
+ `Schema-defined attributes. Keys are matched case-insensitively against the node type's schema attributes and stored in canonical form; unknown attributes or invalid select values reject the whole batch. Date attributes accept "YYYY-MM-DD" or { start, end? }.`
800
+ )
801
+ });
802
+ function registerAddNode(server2, client2) {
803
+ server2.registerTool(
804
+ "naumu_add_node",
805
+ {
806
+ title: "Add Nodes (bulk)",
807
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
808
+ 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.',
809
+ inputSchema: z13.object({
810
+ graphId: z13.string().describe("The graph ID"),
811
+ nodes: z13.array(NodeInput).min(1).max(25).describe("Batch of 1\u201325 nodes to create. Keep batches small for atomicity.")
812
+ })
813
+ },
814
+ async ({ graphId, nodes }) => {
815
+ let schema = null;
816
+ if (nodes.some((n) => n.attributes && Object.keys(n.attributes).length > 0)) {
817
+ const schemaRes = await client2.get(`/api/graphs/${graphId}/schema`);
818
+ schema = JSON.parse(schemaRes.definition);
819
+ }
820
+ const errors = [];
821
+ const payload = nodes.map(({ label, type, content, attributes }, i) => {
822
+ const node = { label, type, content };
823
+ if (schema && attributes && Object.keys(attributes).length > 0) {
824
+ const result = normalizeNodeAttributes(schema, type, attributes);
825
+ if (result.ok) {
826
+ Object.assign(node, result.attributes);
827
+ } else {
828
+ errors.push(...result.errors.map((e) => `nodes[${i}] ("${label}"): ${e}`));
829
+ }
830
+ }
831
+ return node;
832
+ });
833
+ if (errors.length > 0) {
834
+ return {
835
+ content: [
836
+ {
837
+ type: "text",
838
+ text: `Attribute validation failed \u2014 no nodes were created:
839
+ ${errors.join("\n")}
840
+
841
+ Use naumu_get_schema to check valid attribute names, types, and values.`
842
+ }
843
+ ],
844
+ isError: true
845
+ };
846
+ }
847
+ const data = await client2.post(`/api/graphs/${graphId}/nodes`, { nodes: payload });
848
+ return {
849
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
850
+ };
851
+ }
852
+ );
853
+ }
854
+
743
855
  // ../mcp-core/src/tools/add-edge.ts
744
- import { z as z13 } from "zod";
745
- var EdgeInput = z13.object({
746
- source: z13.string().describe("Source node ID"),
747
- target: z13.string().describe("Target node ID"),
748
- label: z13.string().describe("Relationship type (e.g. RELATES_TO, SOLVES, TRACKS)"),
749
- isParent: z13.boolean().optional().describe("Whether this is a parent relationship")
856
+ import { z as z14 } from "zod";
857
+ var EdgeInput = z14.object({
858
+ source: z14.string().describe("Source node ID"),
859
+ target: z14.string().describe("Target node ID"),
860
+ label: z14.string().describe("Relationship type (e.g. RELATES_TO, SOLVES, TRACKS)"),
861
+ isParent: z14.boolean().optional().describe("Whether this is a parent relationship")
750
862
  });
751
863
  function registerAddEdge(server2, client2) {
752
864
  server2.registerTool(
@@ -755,9 +867,9 @@ function registerAddEdge(server2, client2) {
755
867
  title: "Add Edges (bulk)",
756
868
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
757
869
  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.",
758
- inputSchema: z13.object({
759
- graphId: z13.string().describe("The graph ID"),
760
- edges: z13.array(EdgeInput).min(1).max(25).describe("Batch of 1\u201325 edges to create. Keep batches small for atomicity.")
870
+ inputSchema: z14.object({
871
+ graphId: z14.string().describe("The graph ID"),
872
+ edges: z14.array(EdgeInput).min(1).max(25).describe("Batch of 1\u201325 edges to create. Keep batches small for atomicity.")
761
873
  })
762
874
  },
763
875
  async ({ graphId, edges }) => {
@@ -776,7 +888,7 @@ function registerAddEdge(server2, client2) {
776
888
  }
777
889
 
778
890
  // ../mcp-core/src/tools/remove-node.ts
779
- import { z as z14 } from "zod";
891
+ import { z as z15 } from "zod";
780
892
  function registerRemoveNode(server2, client2) {
781
893
  server2.registerTool(
782
894
  "naumu_remove_node",
@@ -784,9 +896,9 @@ function registerRemoveNode(server2, client2) {
784
896
  title: "Remove Node",
785
897
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
786
898
  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.",
787
- inputSchema: z14.object({
788
- graphId: z14.string().describe("The graph ID"),
789
- nodeId: z14.string().describe("The node ID to delete")
899
+ inputSchema: z15.object({
900
+ graphId: z15.string().describe("The graph ID"),
901
+ nodeId: z15.string().describe("The node ID to delete")
790
902
  })
791
903
  },
792
904
  async ({ graphId, nodeId }) => {
@@ -799,7 +911,7 @@ function registerRemoveNode(server2, client2) {
799
911
  }
800
912
 
801
913
  // ../mcp-core/src/tools/remove-edge.ts
802
- import { z as z15 } from "zod";
914
+ import { z as z16 } from "zod";
803
915
  function registerRemoveEdge(server2, client2) {
804
916
  server2.registerTool(
805
917
  "naumu_remove_edge",
@@ -807,11 +919,11 @@ function registerRemoveEdge(server2, client2) {
807
919
  title: "Remove Edge",
808
920
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
809
921
  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.",
810
- inputSchema: z15.object({
811
- graphId: z15.string().describe("The graph ID"),
812
- source: z15.string().describe("Source node id of the edge to delete"),
813
- target: z15.string().describe("Target node id of the edge to delete"),
814
- label: z15.string().describe('Relation label of the edge to delete (e.g. "AUTHORED"). Case-insensitive; non-alphanum chars are normalized.')
922
+ inputSchema: z16.object({
923
+ graphId: z16.string().describe("The graph ID"),
924
+ source: z16.string().describe("Source node id of the edge to delete"),
925
+ target: z16.string().describe("Target node id of the edge to delete"),
926
+ label: z16.string().describe('Relation label of the edge to delete (e.g. "AUTHORED"). Case-insensitive; non-alphanum chars are normalized.')
815
927
  })
816
928
  },
817
929
  async ({ graphId, source, target, label }) => {
@@ -828,11 +940,11 @@ function registerRemoveEdge(server2, client2) {
828
940
  }
829
941
 
830
942
  // ../mcp-core/src/tools/remove-edges-bulk.ts
831
- import { z as z16 } from "zod";
832
- var EdgeRef = z16.object({
833
- source: z16.string().describe("Source node ID"),
834
- target: z16.string().describe("Target node ID"),
835
- label: z16.string().describe("Relation label of the edge to delete")
943
+ import { z as z17 } from "zod";
944
+ var EdgeRef = z17.object({
945
+ source: z17.string().describe("Source node ID"),
946
+ target: z17.string().describe("Target node ID"),
947
+ label: z17.string().describe("Relation label of the edge to delete")
836
948
  });
837
949
  function registerRemoveEdgesBulk(server2, client2) {
838
950
  server2.registerTool(
@@ -841,9 +953,9 @@ function registerRemoveEdgesBulk(server2, client2) {
841
953
  title: "Remove Edges (bulk)",
842
954
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
843
955
  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.",
844
- inputSchema: z16.object({
845
- graphId: z16.string().describe("The graph ID"),
846
- edges: z16.array(EdgeRef).min(1).max(100).describe("1-100 edges to delete. Atomic per call - all succeed or none do.")
956
+ inputSchema: z17.object({
957
+ graphId: z17.string().describe("The graph ID"),
958
+ edges: z17.array(EdgeRef).min(1).max(100).describe("1-100 edges to delete. Atomic per call - all succeed or none do.")
847
959
  })
848
960
  },
849
961
  async ({ graphId, edges }) => {
@@ -856,7 +968,7 @@ function registerRemoveEdgesBulk(server2, client2) {
856
968
  }
857
969
 
858
970
  // ../mcp-core/src/tools/ask.ts
859
- import { z as z17 } from "zod";
971
+ import { z as z18 } from "zod";
860
972
  function registerAsk(server2, client2) {
861
973
  server2.registerTool(
862
974
  "naumu_ask",
@@ -872,14 +984,20 @@ function registerAsk(server2, client2) {
872
984
  openWorldHint: true
873
985
  },
874
986
  description: 'Ask @Naumu a question about a space and get back a synthesised, node-grounded answer with the exact source node ids it used and a confidence hint. This is THE tool for any question 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 has already inspected the graph for you, so its answer is authoritative: present it and its cited sources directly. Do NOT independently re-read the graph (naumu_get_schema, naumu_filter, naumu_get_node, fetch) to verify or flesh out the answer - that repeats work @Naumu already did and is far slower. Reach for a granular read ONLY to pull a specific node the answer pointed to but did not fully include. Returns { answer, sources, confidence, threadId, status }; the answer is saved as a visible conversation in the space, which is expected and useful, so do not avoid the tool to prevent creating a thread. On a very long synthesis it may return status "processing" with a threadId - in that case wait a few seconds and call naumu_read_thread with that threadId for the final answer, and still do not fall back to manual digging. To hand @Naumu work to carry out (add knowledge, make changes, record status) without waiting, use naumu_delegate instead.',
875
- inputSchema: z17.object({
876
- graphId: z17.string().describe("The space (graph) id to ask about."),
877
- question: z17.string().max(4e3).describe("The question for @Naumu. Up to 4000 characters.")
987
+ inputSchema: z18.object({
988
+ graphId: z18.string().describe("The space (graph) id to ask about."),
989
+ question: z18.string().max(4e3).describe("The question for @Naumu. Up to 4000 characters."),
990
+ topicIds: z18.array(z18.string()).max(8).optional().describe(
991
+ "Topic ids (from naumu_list_topics) to file the resulting conversation into, making it visible to those topics' members from birth. Omit to keep the default behavior (the ask lands in the virtual #misc bucket)."
992
+ )
878
993
  })
879
994
  },
880
- async ({ graphId, question }) => {
995
+ async ({ graphId, question, topicIds }) => {
881
996
  try {
882
- const data = await client2.post(`/api/graphs/${graphId}/ask`, { question });
997
+ const data = await client2.post(`/api/graphs/${graphId}/ask`, {
998
+ question,
999
+ ...topicIds && topicIds.length > 0 ? { topicIds } : {}
1000
+ });
883
1001
  return {
884
1002
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
885
1003
  };
@@ -895,7 +1013,7 @@ function registerAsk(server2, client2) {
895
1013
  }
896
1014
 
897
1015
  // ../mcp-core/src/tools/delegate.ts
898
- import { z as z18 } from "zod";
1016
+ import { z as z19 } from "zod";
899
1017
  function registerDelegate(server2, client2) {
900
1018
  server2.registerTool(
901
1019
  "naumu_delegate",
@@ -910,23 +1028,30 @@ function registerDelegate(server2, client2) {
910
1028
  idempotentHint: false,
911
1029
  openWorldHint: true
912
1030
  },
913
- 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.',
914
- inputSchema: z18.object({
915
- graphId: z18.string().describe("The space (graph) id to act in."),
916
- task: z18.string().describe("What you want @Naumu to do, add, or record."),
917
- threadId: z18.string().optional().describe("Continue an existing conversation; omit to start a new one.")
1031
+ 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. File the new thread into topics by passing `topicIds` (see naumu_list_topics): omitting them keeps the default private thread, providing them makes it visible to those topics\' members from birth. This always invokes @Naumu, even in threads where auto-response is paused, so the task text must never contain a literal "@Naumu" mention (plain text never renders or triggers a mention).',
1032
+ inputSchema: z19.object({
1033
+ graphId: z19.string().describe("The space (graph) id to act in."),
1034
+ task: z19.string().describe("What you want @Naumu to do, add, or record."),
1035
+ threadId: z19.string().optional().describe("Continue an existing conversation; omit to start a new one."),
1036
+ topicIds: z19.array(z19.string()).max(8).optional().describe(
1037
+ "Topic ids (from naumu_list_topics) to file a NEW thread into, making it visible to those topics' members. Only used at creation; never adds topics to an existing thread, so it is ignored when threadId is provided. Omit to keep the default private thread."
1038
+ )
918
1039
  })
919
1040
  },
920
- async ({ graphId, task, threadId }) => {
1041
+ async ({ graphId, task, threadId, topicIds }) => {
921
1042
  try {
922
1043
  let resolvedThreadId = threadId;
923
1044
  if (!resolvedThreadId) {
924
- const thread = await client2.post("/api/threads", { graphId });
1045
+ const thread = await client2.post("/api/threads", {
1046
+ graphId,
1047
+ ...topicIds && topicIds.length > 0 ? { topicIds } : {}
1048
+ });
925
1049
  resolvedThreadId = thread.id;
926
1050
  }
927
1051
  await client2.post(`/api/threads/${resolvedThreadId}/messages`, {
928
1052
  content: task,
929
- async: true
1053
+ async: true,
1054
+ invokeAgent: true
930
1055
  });
931
1056
  return {
932
1057
  content: [
@@ -952,7 +1077,7 @@ function registerDelegate(server2, client2) {
952
1077
  }
953
1078
 
954
1079
  // ../mcp-core/src/tools/post-message.ts
955
- import { z as z19 } from "zod";
1080
+ import { z as z20 } from "zod";
956
1081
  function registerPostMessage(server2, client2) {
957
1082
  server2.registerTool(
958
1083
  "naumu_post_message",
@@ -966,12 +1091,12 @@ function registerPostMessage(server2, client2) {
966
1091
  idempotentHint: false,
967
1092
  openWorldHint: false
968
1093
  },
969
- 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.',
970
- inputSchema: z19.object({
971
- threadId: z19.string().describe("The thread ID to post into. You must be a participant in this thread."),
972
- content: z19.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.'),
973
- contentFormat: z19.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 } }`.'),
974
- attachmentIds: z19.array(z19.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.")
1094
+ 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 (mention notifications, prompts) without invoking @Naumu. For a human, the mention `id` is their User id (from naumu_list_members or naumu_get_thread participantDetails) or their email - both work; for a bot/agent, use its identity id. 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.',
1095
+ inputSchema: z20.object({
1096
+ threadId: z20.string().describe("The thread ID to post into. You must be a participant in this thread."),
1097
+ content: z20.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.'),
1098
+ contentFormat: z20.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: userIdOrEmailOrIdentityId, label: displayName } }` (a human mention id is their User id or email; a bot mention id is its identity id).'),
1099
+ attachmentIds: z20.array(z20.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.")
975
1100
  })
976
1101
  },
977
1102
  async ({ threadId, content, contentFormat, attachmentIds }) => {
@@ -997,7 +1122,7 @@ function registerPostMessage(server2, client2) {
997
1122
  }
998
1123
 
999
1124
  // ../mcp-core/src/tools/read-thread.ts
1000
- import { z as z20 } from "zod";
1125
+ import { z as z21 } from "zod";
1001
1126
  function registerReadThread(server2, client2) {
1002
1127
  server2.registerTool(
1003
1128
  "naumu_read_thread",
@@ -1005,10 +1130,10 @@ function registerReadThread(server2, client2) {
1005
1130
  title: "Read Thread",
1006
1131
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1007
1132
  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.',
1008
- inputSchema: z20.object({
1009
- threadId: z20.string().describe("The thread ID to read from."),
1010
- before: z20.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
1011
- limit: z20.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1133
+ inputSchema: z21.object({
1134
+ threadId: z21.string().describe("The thread ID to read from."),
1135
+ before: z21.number().optional().describe("Unix timestamp in milliseconds. Returns messages strictly older than this. Omit for the newest page."),
1136
+ limit: z21.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1012
1137
  })
1013
1138
  },
1014
1139
  async ({ threadId, before, limit }) => {
@@ -1034,7 +1159,7 @@ function registerReadThread(server2, client2) {
1034
1159
  }
1035
1160
 
1036
1161
  // ../mcp-core/src/tools/whoami.ts
1037
- import { z as z21 } from "zod";
1162
+ import { z as z22 } from "zod";
1038
1163
  function registerWhoami(server2, client2, allToolNames) {
1039
1164
  server2.registerTool(
1040
1165
  "naumu_whoami",
@@ -1042,7 +1167,7 @@ function registerWhoami(server2, client2, allToolNames) {
1042
1167
  title: "Who Am I",
1043
1168
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1044
1169
  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.',
1045
- inputSchema: z21.object({})
1170
+ inputSchema: z22.object({})
1046
1171
  },
1047
1172
  async () => {
1048
1173
  try {
@@ -1065,7 +1190,7 @@ function registerWhoami(server2, client2, allToolNames) {
1065
1190
  }
1066
1191
 
1067
1192
  // ../mcp-core/src/tools/list-threads.ts
1068
- import { z as z22 } from "zod";
1193
+ import { z as z23 } from "zod";
1069
1194
  function sanitizeThreadParticipants(thread) {
1070
1195
  if (!thread || typeof thread !== "object" || !("participantEmails" in thread)) {
1071
1196
  return thread;
@@ -1080,10 +1205,10 @@ function registerListThreads(server2, client2) {
1080
1205
  title: "List Threads",
1081
1206
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1082
1207
  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.",
1083
- inputSchema: z22.object({
1084
- graphId: z22.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1085
- cursor: z22.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1086
- limit: z22.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1208
+ inputSchema: z23.object({
1209
+ graphId: z23.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1210
+ cursor: z23.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1211
+ limit: z23.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1087
1212
  })
1088
1213
  },
1089
1214
  async ({ graphId, cursor, limit }) => {
@@ -1118,8 +1243,53 @@ function registerListThreads(server2, client2) {
1118
1243
  );
1119
1244
  }
1120
1245
 
1246
+ // ../mcp-core/src/tools/list-topics.ts
1247
+ import { z as z24 } from "zod";
1248
+ function toFilingDestination(topic) {
1249
+ if (!topic || typeof topic !== "object") return null;
1250
+ const t = topic;
1251
+ if (typeof t.id !== "string" || typeof t.name !== "string") return null;
1252
+ return {
1253
+ id: t.id,
1254
+ name: t.name,
1255
+ ...typeof t.visibilityMode === "string" ? { visibilityMode: t.visibilityMode } : {},
1256
+ ...typeof t.memberCount === "number" ? { memberCount: t.memberCount } : {},
1257
+ ...typeof t.openToWeb === "boolean" ? { openToWeb: t.openToWeb } : {},
1258
+ ...typeof t.isMember === "boolean" ? { isMember: t.isMember } : {}
1259
+ };
1260
+ }
1261
+ function registerListTopics(server2, client2) {
1262
+ server2.registerTool(
1263
+ "naumu_list_topics",
1264
+ {
1265
+ title: "List Topics",
1266
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1267
+ description: "List a space's real topics \u2014 the filing destinations you can hand to the `topicIds` param of naumu_delegate or naumu_ask when creating a thread. Filing a new thread into one or more topics makes it visible to those topics' members from birth (instead of the default private thread). Each row carries { id, name, visibilityMode, memberCount, openToWeb }; use the id values in `topicIds`. Topics have no separate description field. The virtual #misc bucket is not a real topic and is omitted; archived topics are omitted because they can't be tagged. Resolve `graphId` via naumu_list_graphs first.",
1268
+ inputSchema: z24.object({
1269
+ graphId: z24.string().describe("The space (graph) id to list topics for.")
1270
+ })
1271
+ },
1272
+ async ({ graphId }) => {
1273
+ try {
1274
+ const data = await client2.get(`/api/graphs/${graphId}/topics`);
1275
+ const rows = Array.isArray(data) ? data : [];
1276
+ const topics = rows.filter((t) => !(t && typeof t === "object" && t.archived === true)).map(toFilingDestination).filter((t) => t !== null);
1277
+ return {
1278
+ content: [{ type: "text", text: JSON.stringify(topics, null, 2) }]
1279
+ };
1280
+ } catch (err) {
1281
+ const message = err instanceof Error ? err.message : String(err);
1282
+ return {
1283
+ content: [{ type: "text", text: `Error: ${message}` }],
1284
+ isError: true
1285
+ };
1286
+ }
1287
+ }
1288
+ );
1289
+ }
1290
+
1121
1291
  // ../mcp-core/src/tools/get-thread.ts
1122
- import { z as z23 } from "zod";
1292
+ import { z as z25 } from "zod";
1123
1293
  function sanitizeThreadParticipants2(thread) {
1124
1294
  if (!thread || typeof thread !== "object" || !("participantEmails" in thread)) {
1125
1295
  return thread;
@@ -1134,8 +1304,8 @@ function registerGetThread(server2, client2) {
1134
1304
  title: "Get Thread",
1135
1305
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1136
1306
  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.",
1137
- inputSchema: z23.object({
1138
- threadId: z23.string().describe("The thread ID to fetch.")
1307
+ inputSchema: z25.object({
1308
+ threadId: z25.string().describe("The thread ID to fetch.")
1139
1309
  })
1140
1310
  },
1141
1311
  async ({ threadId }) => {
@@ -1157,7 +1327,7 @@ function registerGetThread(server2, client2) {
1157
1327
  }
1158
1328
 
1159
1329
  // ../mcp-core/src/tools/create-thread.ts
1160
- import { z as z24 } from "zod";
1330
+ import { z as z26 } from "zod";
1161
1331
  function registerCreateThread(server2, client2) {
1162
1332
  server2.registerTool(
1163
1333
  "naumu_create_thread",
@@ -1171,32 +1341,36 @@ function registerCreateThread(server2, client2) {
1171
1341
  idempotentHint: false,
1172
1342
  openWorldHint: false
1173
1343
  },
1174
- 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.",
1175
- inputSchema: z24.object({
1176
- title: z24.string().min(1).max(200).optional().describe('Thread title shown in the sidebar. If omitted, Naumu generates a default like "Conversation YYYY-MM-DD".'),
1177
- participants: z24.array(
1178
- z24.discriminatedUnion("type", [
1179
- z24.object({
1180
- type: z24.literal("user"),
1181
- userId: z24.string().min(1).describe("User UUID \u2014 get these from `naumu_get_thread`/`naumu_read_thread` participant rosters or webhook payloads.")
1344
+ 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. Optional `topicIds` files the new thread into one or more topics (see naumu_list_topics for ids): it becomes visible to those topics' members from birth instead of staying a private thread between you and your owner. Filing is creation-only and only ever widens - it never removes the thread from a topic later. 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.",
1345
+ inputSchema: z26.object({
1346
+ title: z26.string().min(1).max(200).optional().describe('Thread title shown in the sidebar. If omitted, Naumu generates a default like "Conversation YYYY-MM-DD".'),
1347
+ participants: z26.array(
1348
+ z26.discriminatedUnion("type", [
1349
+ z26.object({
1350
+ type: z26.literal("user"),
1351
+ userId: z26.string().min(1).describe("User UUID \u2014 get these from `naumu_get_thread`/`naumu_read_thread` participant rosters or webhook payloads.")
1182
1352
  }),
1183
- z24.object({
1184
- type: z24.literal("identity"),
1185
- identityId: z24.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.")
1353
+ z26.object({
1354
+ type: z26.literal("identity"),
1355
+ identityId: z26.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.")
1186
1356
  })
1187
1357
  ])
1188
1358
  ).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."),
1189
- initialMessage: z24.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."),
1190
- visibility: z24.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.")
1359
+ initialMessage: z26.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."),
1360
+ visibility: z26.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."),
1361
+ topicIds: z26.array(z26.string()).max(8).optional().describe(
1362
+ "Topic ids (from naumu_list_topics) to file the NEW thread into, making it visible to those topics' members from birth. Creation-only and only ever widens - it never removes the thread from a topic later. Omit to keep the default private thread between you and your owner."
1363
+ )
1191
1364
  })
1192
1365
  },
1193
- async ({ title, participants, initialMessage, visibility }) => {
1366
+ async ({ title, participants, initialMessage, visibility, topicIds }) => {
1194
1367
  try {
1195
1368
  const body = {};
1196
1369
  if (title !== void 0) body.title = title;
1197
1370
  if (participants !== void 0) body.participants = participants;
1198
1371
  if (initialMessage !== void 0) body.initialMessage = initialMessage;
1199
1372
  if (visibility !== void 0) body.visibility = visibility;
1373
+ if (topicIds !== void 0) body.topicIds = topicIds;
1200
1374
  const data = await client2.post("/api/identities/me/threads", body);
1201
1375
  return {
1202
1376
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
@@ -1213,7 +1387,7 @@ function registerCreateThread(server2, client2) {
1213
1387
  }
1214
1388
 
1215
1389
  // ../mcp-core/src/tools/request-attachment-upload.ts
1216
- import { z as z25 } from "zod";
1390
+ import { z as z27 } from "zod";
1217
1391
  function registerRequestAttachmentUpload(server2, client2) {
1218
1392
  server2.registerTool(
1219
1393
  "naumu_request_attachment_upload",
@@ -1221,12 +1395,12 @@ function registerRequestAttachmentUpload(server2, client2) {
1221
1395
  title: "Request Attachment Upload",
1222
1396
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1223
1397
  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.',
1224
- inputSchema: z25.object({
1225
- threadId: z25.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."),
1226
- fileName: z25.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."),
1227
- fileType: z25.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."),
1228
- fileSize: z25.number().int().positive().describe("File size in bytes. Validated against per-MIME caps before the URL is issued - exceeding the cap returns a 400."),
1229
- audioDurationSec: z25.number().positive().optional().describe("For audio attachments, duration in seconds. Validated against the audio recording cap (currently 8 hours).")
1398
+ inputSchema: z27.object({
1399
+ threadId: z27.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."),
1400
+ fileName: z27.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."),
1401
+ fileType: z27.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."),
1402
+ fileSize: z27.number().int().positive().describe("File size in bytes. Validated against per-MIME caps before the URL is issued - exceeding the cap returns a 400."),
1403
+ audioDurationSec: z27.number().positive().optional().describe("For audio attachments, duration in seconds. Validated against the audio recording cap (currently 8 hours).")
1230
1404
  })
1231
1405
  },
1232
1406
  async ({ threadId, fileName, fileType, fileSize, audioDurationSec }) => {
@@ -1256,7 +1430,7 @@ function registerRequestAttachmentUpload(server2, client2) {
1256
1430
  }
1257
1431
 
1258
1432
  // ../mcp-core/src/tools/add-reaction.ts
1259
- import { z as z26 } from "zod";
1433
+ import { z as z28 } from "zod";
1260
1434
  function registerAddReaction(server2, client2) {
1261
1435
  server2.registerTool(
1262
1436
  "naumu_add_reaction",
@@ -1264,10 +1438,10 @@ function registerAddReaction(server2, client2) {
1264
1438
  title: "Add Reaction",
1265
1439
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1266
1440
  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.',
1267
- inputSchema: z26.object({
1268
- threadId: z26.string().describe("Thread containing the message. You must be a participant."),
1269
- messageId: z26.string().describe("The message to react to."),
1270
- emoji: z26.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.')
1441
+ inputSchema: z28.object({
1442
+ threadId: z28.string().describe("Thread containing the message. You must be a participant."),
1443
+ messageId: z28.string().describe("The message to react to."),
1444
+ emoji: z28.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.')
1271
1445
  })
1272
1446
  },
1273
1447
  async ({ threadId, messageId, emoji }) => {
@@ -1291,7 +1465,7 @@ function registerAddReaction(server2, client2) {
1291
1465
  }
1292
1466
 
1293
1467
  // ../mcp-core/src/tools/remove-reaction.ts
1294
- import { z as z27 } from "zod";
1468
+ import { z as z29 } from "zod";
1295
1469
  function registerRemoveReaction(server2, client2) {
1296
1470
  server2.registerTool(
1297
1471
  "naumu_remove_reaction",
@@ -1299,10 +1473,10 @@ function registerRemoveReaction(server2, client2) {
1299
1473
  title: "Remove Reaction",
1300
1474
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1301
1475
  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.",
1302
- inputSchema: z27.object({
1303
- threadId: z27.string().describe("Thread containing the message. You must be a participant."),
1304
- messageId: z27.string().describe("The message to remove your reaction from."),
1305
- emoji: z27.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1476
+ inputSchema: z29.object({
1477
+ threadId: z29.string().describe("Thread containing the message. You must be a participant."),
1478
+ messageId: z29.string().describe("The message to remove your reaction from."),
1479
+ emoji: z29.string().min(1).describe("Emoji character to remove (must match what you originally reacted with).")
1306
1480
  })
1307
1481
  },
1308
1482
  async ({ threadId, messageId, emoji }) => {
@@ -1326,7 +1500,7 @@ function registerRemoveReaction(server2, client2) {
1326
1500
  }
1327
1501
 
1328
1502
  // ../mcp-core/src/tools/naumu-typing.ts
1329
- import { z as z28 } from "zod";
1503
+ import { z as z30 } from "zod";
1330
1504
  function registerNaumuTyping(server2, client2) {
1331
1505
  server2.registerTool(
1332
1506
  "naumu_typing",
@@ -1337,9 +1511,9 @@ function registerNaumuTyping(server2, client2) {
1337
1511
  // repeating the same state is a no-op renew, so idempotent.
1338
1512
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1339
1513
  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.',
1340
- inputSchema: z28.object({
1341
- threadId: z28.string().describe("The thread ID to set typing in. You must be a participant."),
1342
- state: z28.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1514
+ inputSchema: z30.object({
1515
+ threadId: z30.string().describe("The thread ID to set typing in. You must be a participant."),
1516
+ state: z30.enum(["start", "stop"]).describe('"start" acquires/renews the lease; "stop" ends it and clears the pill immediately.')
1343
1517
  })
1344
1518
  },
1345
1519
  async ({ threadId, state }) => {
@@ -1360,7 +1534,7 @@ function registerNaumuTyping(server2, client2) {
1360
1534
  }
1361
1535
 
1362
1536
  // ../mcp-core/src/tools/note-read.ts
1363
- import { z as z29 } from "zod";
1537
+ import { z as z31 } from "zod";
1364
1538
  function registerNoteRead(server2, client2) {
1365
1539
  server2.registerTool(
1366
1540
  "naumu_note_read",
@@ -1368,8 +1542,8 @@ function registerNoteRead(server2, client2) {
1368
1542
  title: "Read Note",
1369
1543
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1370
1544
  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.",
1371
- inputSchema: z29.object({
1372
- noteId: z29.string().describe("The note (Thought) ID")
1545
+ inputSchema: z31.object({
1546
+ noteId: z31.string().describe("The note (Thought) ID")
1373
1547
  })
1374
1548
  },
1375
1549
  async ({ noteId }) => {
@@ -1382,7 +1556,7 @@ function registerNoteRead(server2, client2) {
1382
1556
  }
1383
1557
 
1384
1558
  // ../mcp-core/src/tools/note-append.ts
1385
- import { z as z30 } from "zod";
1559
+ import { z as z32 } from "zod";
1386
1560
  function registerNoteAppend(server2, client2) {
1387
1561
  server2.registerTool(
1388
1562
  "naumu_note_append",
@@ -1390,9 +1564,9 @@ function registerNoteAppend(server2, client2) {
1390
1564
  title: "Append to Note",
1391
1565
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1392
1566
  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.",
1393
- inputSchema: z30.object({
1394
- noteId: z30.string().describe("The note (Thought) ID to append to"),
1395
- markdown: z30.string().min(1).describe("Markdown content to append at the end of the note")
1567
+ inputSchema: z32.object({
1568
+ noteId: z32.string().describe("The note (Thought) ID to append to"),
1569
+ markdown: z32.string().min(1).describe("Markdown content to append at the end of the note")
1396
1570
  })
1397
1571
  },
1398
1572
  async ({ noteId, markdown }) => {
@@ -1405,7 +1579,7 @@ function registerNoteAppend(server2, client2) {
1405
1579
  }
1406
1580
 
1407
1581
  // ../mcp-core/src/tools/note-insert.ts
1408
- import { z as z31 } from "zod";
1582
+ import { z as z33 } from "zod";
1409
1583
  function registerNoteInsert(server2, client2) {
1410
1584
  server2.registerTool(
1411
1585
  "naumu_note_insert",
@@ -1413,10 +1587,10 @@ function registerNoteInsert(server2, client2) {
1413
1587
  title: "Insert After Heading",
1414
1588
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1415
1589
  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.",
1416
- inputSchema: z31.object({
1417
- noteId: z31.string().describe("The note (Thought) ID"),
1418
- headingText: z31.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1419
- markdown: z31.string().min(1).describe("Markdown content to insert at the end of that section")
1590
+ inputSchema: z33.object({
1591
+ noteId: z33.string().describe("The note (Thought) ID"),
1592
+ headingText: z33.string().min(1).describe("Exact text of the heading whose section the new content follows"),
1593
+ markdown: z33.string().min(1).describe("Markdown content to insert at the end of that section")
1420
1594
  })
1421
1595
  },
1422
1596
  async ({ noteId, headingText, markdown }) => {
@@ -1432,7 +1606,7 @@ function registerNoteInsert(server2, client2) {
1432
1606
  }
1433
1607
 
1434
1608
  // ../mcp-core/src/tools/note-replace-section.ts
1435
- import { z as z32 } from "zod";
1609
+ import { z as z34 } from "zod";
1436
1610
  function registerNoteReplaceSection(server2, client2) {
1437
1611
  server2.registerTool(
1438
1612
  "naumu_note_replace_section",
@@ -1440,11 +1614,11 @@ function registerNoteReplaceSection(server2, client2) {
1440
1614
  title: "Replace Section",
1441
1615
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1442
1616
  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.",
1443
- inputSchema: z32.object({
1444
- noteId: z32.string().describe("The note (Thought) ID"),
1445
- headingText: z32.string().min(1).describe("Exact text of the heading anchoring the section"),
1446
- markdown: z32.string().describe("Replacement markdown for the section body"),
1447
- keepHeading: z32.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1617
+ inputSchema: z34.object({
1618
+ noteId: z34.string().describe("The note (Thought) ID"),
1619
+ headingText: z34.string().min(1).describe("Exact text of the heading anchoring the section"),
1620
+ markdown: z34.string().describe("Replacement markdown for the section body"),
1621
+ keepHeading: z34.boolean().optional().describe("Whether to keep the heading row itself. Default true.")
1448
1622
  })
1449
1623
  },
1450
1624
  async ({ noteId, headingText, markdown, keepHeading }) => {
@@ -1461,7 +1635,7 @@ function registerNoteReplaceSection(server2, client2) {
1461
1635
  }
1462
1636
 
1463
1637
  // ../mcp-core/src/tools/note-delete-section.ts
1464
- import { z as z33 } from "zod";
1638
+ import { z as z35 } from "zod";
1465
1639
  function registerNoteDeleteSection(server2, client2) {
1466
1640
  server2.registerTool(
1467
1641
  "naumu_note_delete_section",
@@ -1469,9 +1643,9 @@ function registerNoteDeleteSection(server2, client2) {
1469
1643
  title: "Delete Section",
1470
1644
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1471
1645
  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.",
1472
- inputSchema: z33.object({
1473
- noteId: z33.string().describe("The note (Thought) ID"),
1474
- headingText: z33.string().min(1).describe("Exact text of the heading whose section will be deleted")
1646
+ inputSchema: z35.object({
1647
+ noteId: z35.string().describe("The note (Thought) ID"),
1648
+ headingText: z35.string().min(1).describe("Exact text of the heading whose section will be deleted")
1475
1649
  })
1476
1650
  },
1477
1651
  async ({ noteId, headingText }) => {
@@ -1486,7 +1660,7 @@ function registerNoteDeleteSection(server2, client2) {
1486
1660
  }
1487
1661
 
1488
1662
  // ../mcp-core/src/tools/note-replace.ts
1489
- import { z as z34 } from "zod";
1663
+ import { z as z36 } from "zod";
1490
1664
  function registerNoteReplace(server2, client2) {
1491
1665
  server2.registerTool(
1492
1666
  "naumu_note_replace",
@@ -1494,9 +1668,9 @@ function registerNoteReplace(server2, client2) {
1494
1668
  title: "Replace Note",
1495
1669
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1496
1670
  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.",
1497
- inputSchema: z34.object({
1498
- noteId: z34.string().describe("The note (Thought) ID"),
1499
- markdown: z34.string().describe("New markdown content for the entire note")
1671
+ inputSchema: z36.object({
1672
+ noteId: z36.string().describe("The note (Thought) ID"),
1673
+ markdown: z36.string().describe("New markdown content for the entire note")
1500
1674
  })
1501
1675
  },
1502
1676
  async ({ noteId, markdown }) => {
@@ -1509,7 +1683,7 @@ function registerNoteReplace(server2, client2) {
1509
1683
  }
1510
1684
 
1511
1685
  // ../mcp-core/src/tools/note-find-replace.ts
1512
- import { z as z35 } from "zod";
1686
+ import { z as z37 } from "zod";
1513
1687
  function registerNoteFindReplace(server2, client2) {
1514
1688
  server2.registerTool(
1515
1689
  "naumu_note_find_replace",
@@ -1517,11 +1691,11 @@ function registerNoteFindReplace(server2, client2) {
1517
1691
  title: "Find/Replace in Note",
1518
1692
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
1519
1693
  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.",
1520
- inputSchema: z35.object({
1521
- noteId: z35.string().describe("The note (Thought) ID"),
1522
- find: z35.string().min(1).describe("Substring to search for. Literal - no regex."),
1523
- replace: z35.string().describe("Replacement string. May be empty to delete the match."),
1524
- all: z35.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1694
+ inputSchema: z37.object({
1695
+ noteId: z37.string().describe("The note (Thought) ID"),
1696
+ find: z37.string().min(1).describe("Substring to search for. Literal - no regex."),
1697
+ replace: z37.string().describe("Replacement string. May be empty to delete the match."),
1698
+ all: z37.boolean().optional().describe("Replace all occurrences (default true). Pass false to replace only the first.")
1525
1699
  })
1526
1700
  },
1527
1701
  async ({ noteId, find, replace, all }) => {
@@ -1538,21 +1712,23 @@ function registerNoteFindReplace(server2, client2) {
1538
1712
  }
1539
1713
 
1540
1714
  // ../mcp-core/src/tools/create-note.ts
1541
- import { z as z36 } from "zod";
1715
+ import { z as z38 } from "zod";
1542
1716
  function registerCreateNote(server2, client2) {
1543
1717
  server2.registerTool(
1544
1718
  "naumu_create_note",
1545
1719
  {
1546
1720
  title: "Create Note",
1547
1721
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1548
- 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.",
1549
- inputSchema: z36.object({
1550
- graphId: z36.string().describe("The graph ID to create the note in"),
1551
- title: z36.string().optional().describe("Optional title for the note")
1722
+ description: "Create a new empty note in a graph; use when you need a fresh note to write into. Returns the new note row including its `id` - pass that id to `naumu_note_append` / `naumu_note_replace` to fill in the content. Bots can only create notes in their own graph, and a bot-created note is private (participants only) unless `sharedWithSpace` is set true. Bots cannot file notes into topics \u2014 a bot passing `topicIds` is rejected (bots hold no topic membership); use `sharedWithSpace` instead.",
1723
+ inputSchema: z38.object({
1724
+ graphId: z38.string().describe("The graph ID to create the note in"),
1725
+ title: z38.string().optional().describe("Optional title for the note"),
1726
+ sharedWithSpace: z38.boolean().optional().describe("Share the note with everyone in the space. Omit (or false) to keep it private to its participants."),
1727
+ topicIds: z38.array(z38.string()).max(8).optional().describe("File the note into these topics (get ids from naumu_list_topics), making it visible to those topics' members. Not available to bots.")
1552
1728
  })
1553
1729
  },
1554
- async ({ graphId, title }) => {
1555
- const data = await client2.post("/api/notes", { graphId, title });
1730
+ async ({ graphId, title, sharedWithSpace, topicIds }) => {
1731
+ const data = await client2.post("/api/notes", { graphId, title, sharedWithSpace, topicIds });
1556
1732
  return {
1557
1733
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1558
1734
  };
@@ -1561,7 +1737,7 @@ function registerCreateNote(server2, client2) {
1561
1737
  }
1562
1738
 
1563
1739
  // ../mcp-core/src/tools/list-schema-violations.ts
1564
- import { z as z37 } from "zod";
1740
+ import { z as z39 } from "zod";
1565
1741
  var DEFAULT_EXAMPLE_LIMIT = 5;
1566
1742
  var rowsForKind = (violations, kind) => {
1567
1743
  const rows = [];
@@ -1587,12 +1763,12 @@ function registerListSchemaViolations(server2, client2) {
1587
1763
  title: "List Schema Violations",
1588
1764
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1589
1765
  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.",
1590
- inputSchema: z37.object({
1591
- graphId: z37.string().describe("The graph ID"),
1592
- kind: z37.string().optional().describe(
1766
+ inputSchema: z39.object({
1767
+ graphId: z39.string().describe("The graph ID"),
1768
+ kind: z39.string().optional().describe(
1593
1769
  '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.'
1594
1770
  ),
1595
- limit: z37.number().int().min(1).optional().describe(
1771
+ limit: z39.number().int().min(1).optional().describe(
1596
1772
  "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)."
1597
1773
  )
1598
1774
  })
@@ -1644,7 +1820,7 @@ function registerListSchemaViolations(server2, client2) {
1644
1820
  }
1645
1821
 
1646
1822
  // ../mcp-core/src/tools/list-dense-nodes.ts
1647
- import { z as z38 } from "zod";
1823
+ import { z as z40 } from "zod";
1648
1824
  function registerListDenseNodes(server2, client2) {
1649
1825
  server2.registerTool(
1650
1826
  "naumu_list_dense_nodes",
@@ -1652,10 +1828,10 @@ function registerListDenseNodes(server2, client2) {
1652
1828
  title: "List Dense Nodes",
1653
1829
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1654
1830
  description: 'Return nodes whose child count (children via parent edges; mesh cross-links don\'t count) is \u2265 minConnections, grouped by type; use for /restructure hub detection. Each row includes `same_typed_child_count` - the number of children of the SAME type as the node (the Naumu hub-pattern signal) and `connection_count` - its total children. Sort the response by `same_typed_child_count` descending and route any node with \u226510 same-typed children through a mini-hub split. Pass `nodeTypes` (comma-separated) to restrict to a subset (e.g. ["Type A","Type B"]). Cheap to call - runs a single Cypher aggregation.',
1655
- inputSchema: z38.object({
1656
- graphId: z38.string().describe("The graph ID"),
1657
- minConnections: z38.number().int().min(1).describe("Minimum number of children (parent edges; mesh cross-links excluded). Typical: 10 for hub detection, 11 to count only hubs that exceed the round-4 \u226410 threshold."),
1658
- nodeTypes: z38.array(z38.string()).optional().describe("Optional list of node types to restrict the scan to.")
1831
+ inputSchema: z40.object({
1832
+ graphId: z40.string().describe("The graph ID"),
1833
+ minConnections: z40.number().int().min(1).describe("Minimum number of children (parent edges; mesh cross-links excluded). Typical: 10 for hub detection, 11 to count only hubs that exceed the round-4 \u226410 threshold."),
1834
+ nodeTypes: z40.array(z40.string()).optional().describe("Optional list of node types to restrict the scan to.")
1659
1835
  })
1660
1836
  },
1661
1837
  async ({ graphId, minConnections, nodeTypes }) => {
@@ -1673,7 +1849,7 @@ function registerListDenseNodes(server2, client2) {
1673
1849
  }
1674
1850
 
1675
1851
  // ../mcp-core/src/tools/list-node-connections.ts
1676
- import { z as z39 } from "zod";
1852
+ import { z as z41 } from "zod";
1677
1853
  function registerListNodeConnections(server2, client2) {
1678
1854
  server2.registerTool(
1679
1855
  "naumu_list_node_connections",
@@ -1681,11 +1857,11 @@ function registerListNodeConnections(server2, client2) {
1681
1857
  title: "List Node Connections",
1682
1858
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1683
1859
  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}}] }`.',
1684
- inputSchema: z39.object({
1685
- graphId: z39.string().describe("The graph ID"),
1686
- nodeId: z39.string().describe("The node ID to inspect"),
1687
- edgeType: z39.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
1688
- direction: z39.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
1860
+ inputSchema: z41.object({
1861
+ graphId: z41.string().describe("The graph ID"),
1862
+ nodeId: z41.string().describe("The node ID to inspect"),
1863
+ edgeType: z41.string().optional().describe('Restrict to a specific relation label (e.g. "ASSOCIATED_WITH"). Case-insensitive; non-alphanum chars are normalized.'),
1864
+ direction: z41.enum(["in", "out", "both"]).optional().describe('Edge direction filter - "in" (incoming), "out" (outgoing), "both" (default).')
1689
1865
  })
1690
1866
  },
1691
1867
  async ({ graphId, nodeId, edgeType, direction }) => {
@@ -1703,7 +1879,7 @@ function registerListNodeConnections(server2, client2) {
1703
1879
  }
1704
1880
 
1705
1881
  // ../mcp-core/src/tools/reparent.ts
1706
- import { z as z40 } from "zod";
1882
+ import { z as z42 } from "zod";
1707
1883
  function registerReparent(server2, client2) {
1708
1884
  server2.registerTool(
1709
1885
  "naumu_reparent",
@@ -1711,11 +1887,11 @@ function registerReparent(server2, client2) {
1711
1887
  title: "Reparent Node",
1712
1888
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1713
1889
  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"}`.',
1714
- inputSchema: z40.object({
1715
- graphId: z40.string().describe("The graph ID"),
1716
- nodeId: z40.string().describe("The child node to reparent"),
1717
- newParentId: z40.string().describe("The new parent node id"),
1718
- newRelation: z40.string().describe('The new parent edge relation label (e.g. "PART_OF"). Must be valid per the schema for (child.type, relation, parent.type).')
1890
+ inputSchema: z42.object({
1891
+ graphId: z42.string().describe("The graph ID"),
1892
+ nodeId: z42.string().describe("The child node to reparent"),
1893
+ newParentId: z42.string().describe("The new parent node id"),
1894
+ newRelation: z42.string().describe('The new parent edge relation label (e.g. "PART_OF"). Must be valid per the schema for (child.type, relation, parent.type).')
1719
1895
  })
1720
1896
  },
1721
1897
  async ({ graphId, nodeId, newParentId, newRelation }) => {
@@ -1731,7 +1907,7 @@ function registerReparent(server2, client2) {
1731
1907
  }
1732
1908
 
1733
1909
  // ../mcp-core/src/tools/batch-reparent.ts
1734
- import { z as z41 } from "zod";
1910
+ import { z as z43 } from "zod";
1735
1911
  function registerBatchReparent(server2, client2) {
1736
1912
  server2.registerTool(
1737
1913
  "naumu_batch_reparent",
@@ -1739,11 +1915,11 @@ function registerBatchReparent(server2, client2) {
1739
1915
  title: "Batch Reparent Nodes",
1740
1916
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1741
1917
  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?}]`.',
1742
- inputSchema: z41.object({
1743
- graphId: z41.string().describe("The graph ID"),
1744
- newParentId: z41.string().describe("Parent node id every nodeId in the batch will be parented to"),
1745
- newRelation: z41.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
1746
- nodeIds: z41.array(z41.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
1918
+ inputSchema: z43.object({
1919
+ graphId: z43.string().describe("The graph ID"),
1920
+ newParentId: z43.string().describe("Parent node id every nodeId in the batch will be parented to"),
1921
+ newRelation: z43.string().describe("Parent edge relation label (must be valid per schema for child.type \u2192 parent.type)"),
1922
+ nodeIds: z43.array(z43.string()).min(1).max(25).describe("1\u201325 child node ids to reparent under `newParentId`")
1747
1923
  })
1748
1924
  },
1749
1925
  async ({ graphId, newParentId, newRelation, nodeIds }) => {
@@ -1760,7 +1936,7 @@ function registerBatchReparent(server2, client2) {
1760
1936
  }
1761
1937
 
1762
1938
  // ../mcp-core/src/tools/chatgpt-search.ts
1763
- import { z as z42 } from "zod";
1939
+ import { z as z44 } from "zod";
1764
1940
 
1765
1941
  // ../mcp-core/src/public-origin.ts
1766
1942
  function publicOrigin() {
@@ -1814,8 +1990,8 @@ function registerChatgptSearch(server2, client2) {
1814
1990
  title: "Search",
1815
1991
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1816
1992
  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.)",
1817
- inputSchema: z42.object({
1818
- query: z42.string().describe('Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa").')
1993
+ inputSchema: z44.object({
1994
+ query: z44.string().describe('Search query text. Mix synonyms and exact tokens freely (e.g. "auth login SSO 2fa").')
1819
1995
  })
1820
1996
  },
1821
1997
  async ({ query }) => {
@@ -1847,7 +2023,7 @@ function registerChatgptSearch(server2, client2) {
1847
2023
  }
1848
2024
 
1849
2025
  // ../mcp-core/src/tools/chatgpt-fetch.ts
1850
- import { z as z43 } from "zod";
2026
+ import { z as z45 } from "zod";
1851
2027
  var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
1852
2028
  "id",
1853
2029
  "label",
@@ -1912,8 +2088,8 @@ function registerChatgptFetch(server2, client2) {
1912
2088
  title: "Fetch",
1913
2089
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1914
2090
  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.",
1915
- inputSchema: z43.object({
1916
- id: z43.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
2091
+ inputSchema: z45.object({
2092
+ id: z45.string().describe("A resource id from a previous `search` result, in the form `<graphId>:<nodeId>`.")
1917
2093
  })
1918
2094
  },
1919
2095
  async ({ id }) => {
@@ -1958,6 +2134,7 @@ function registerChatgptFetch(server2, client2) {
1958
2134
  // ../mcp-core/src/tools/index.ts
1959
2135
  var TOOL_REGISTRARS = {
1960
2136
  naumu_list_graphs: registerListGraphs,
2137
+ naumu_list_members: registerListMembers,
1961
2138
  naumu_create_graph: registerCreateGraph,
1962
2139
  naumu_get_schema: registerGetSchema,
1963
2140
  naumu_update_schema: registerUpdateSchema,
@@ -1980,6 +2157,7 @@ var TOOL_REGISTRARS = {
1980
2157
  naumu_read_thread: registerReadThread,
1981
2158
  naumu_whoami: registerWhoami,
1982
2159
  naumu_list_threads: registerListThreads,
2160
+ naumu_list_topics: registerListTopics,
1983
2161
  naumu_get_thread: registerGetThread,
1984
2162
  naumu_create_thread: registerCreateThread,
1985
2163
  naumu_request_attachment_upload: registerRequestAttachmentUpload,
@@ -2006,9 +2184,13 @@ var TOOL_REGISTRARS = {
2006
2184
  search: registerChatgptSearch,
2007
2185
  fetch: registerChatgptFetch
2008
2186
  };
2009
- var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS);
2187
+ var BOT_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set(["naumu_create_thread"]);
2188
+ var ALL_TOOL_NAMES = Object.keys(TOOL_REGISTRARS).filter(
2189
+ (name) => !BOT_ONLY_TOOL_NAMES.has(name)
2190
+ );
2010
2191
  function registerAllTools(server2, client2) {
2011
2192
  for (const [name, registrar] of Object.entries(TOOL_REGISTRARS)) {
2193
+ if (BOT_ONLY_TOOL_NAMES.has(name)) continue;
2012
2194
  if (name === "naumu_whoami") {
2013
2195
  registerWhoami(server2, client2, ALL_TOOL_NAMES);
2014
2196
  } else {