@naumu/mcp 0.11.1 → 0.12.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.
package/README.md CHANGED
@@ -1,14 +1,59 @@
1
1
  # @naumu/mcp
2
2
 
3
- MCP server for [Naumu](https://naumu.ai) access your knowledge graph from Claude Code, Cursor, and other AI coding agents.
3
+ MCP server for [Naumu](https://naumu.ai) - access your knowledge graph from Claude Code, Cursor, and other AI agents.
4
4
 
5
- ## Setup
5
+ Naumu speaks MCP over two transports:
6
+
7
+ 1. **Remote, over OAuth** at `https://naumu.ai/api/mcp` - the recommended path. You sign in through your browser, and there is no API key to create, paste, or keep in sync. This package is not involved.
8
+ 2. **Local stdio**, this package, launched on demand with `npx` and authenticated with an API key. Use it for clients that cannot do browser OAuth: CI jobs, bot and external-agent identities, and older stdio-only harnesses.
9
+
10
+ ## Recommended setup: remote MCP over OAuth
11
+
12
+ Register the server once. The first time a session uses a Naumu tool, your browser opens to sign in and approve access. After that the tools are available in every session.
13
+
14
+ ### Claude Code
15
+
16
+ ```bash
17
+ claude mcp add --transport http --scope user naumu https://naumu.ai/api/mcp
18
+ ```
19
+
20
+ `--scope user` registers the server for your whole machine rather than the current project, so you do not have to repeat it per repository.
21
+
22
+ ### Cursor
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "naumu": {
28
+ "url": "https://naumu.ai/api/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ Leave any OAuth client ID and secret fields blank. Naumu registers the client for you automatically during sign-in, using Dynamic Client Registration (DCR).
35
+
36
+ Other MCP-capable clients that support remote servers follow the same shape: point them at `https://naumu.ai/api/mcp` and let the browser handle sign-in. Use the front-end origin (`naumu.ai`), not a backend address - sign-in and consent rely on your Naumu session, which lives on the front-end origin.
37
+
38
+ You can revoke a connected tool's access at any time in Naumu under **Settings > Your agents**, in the **Active access** list.
39
+
40
+ See [Local MCP](https://naumu.ai/docs/local-mcp) for the full walkthrough.
41
+
42
+ ## Fallback setup: local stdio server with an API key
43
+
44
+ Use this package when your client cannot do browser OAuth - CI, bots and external agent identities, or a harness that only speaks stdio.
6
45
 
7
46
  ### 1. Get an API key
8
47
 
9
- Generate an API key from your Naumu account settings.
48
+ 1. Open **Settings > Your agents** in Naumu.
49
+ 2. Select **Create an API key** and give it a recognizable name (such as the tool it is for).
50
+ 3. Copy the key right away. It is shown only once and cannot be retrieved later. Keys start with `nmu_`.
51
+
52
+ Use a separate key per tool, so a leak only costs you that one key.
10
53
 
11
- ### 2. Configure your IDE
54
+ ### 2. Configure your client
55
+
56
+ You do not install this package globally; your client launches it on demand with `npx`.
12
57
 
13
58
  #### Claude Code
14
59
 
@@ -20,8 +65,6 @@ claude mcp add naumu -s user \
20
65
 
21
66
  #### Cursor
22
67
 
23
- Add to your Cursor MCP settings:
24
-
25
68
  ```json
26
69
  {
27
70
  "mcpServers": {
@@ -36,39 +79,138 @@ Add to your Cursor MCP settings:
36
79
  }
37
80
  ```
38
81
 
82
+ ### Environment variables
83
+
84
+ | Variable | Required | Default | Description |
85
+ |----------|----------|---------|-------------|
86
+ | `NAUMU_API_KEY` | Yes | - | Your API key (starts with `nmu_`) |
87
+ | `NAUMU_API_URL` | No | `https://naumu.ai` | Naumu API base URL. You do not need to set this |
88
+
39
89
  ## Available tools
40
90
 
91
+ Both transports expose the same tool surface. Tools marked **bot** are only registered for bot identity keys (`nmu_bot_*`), which additionally receive a filtered subset based on the identity's permissions.
92
+
93
+ ### Spaces and members
94
+
41
95
  | Tool | Description |
42
96
  |------|-------------|
43
- | `naumu_list_graphs` | List all knowledge graphs you have access to |
44
- | `naumu_get_schema` | Get the schema (node types, relationships) for a graph |
45
- | `naumu_text_search` | Search nodes by exact text match on labels and content |
46
- | `naumu_semantic_search` | Search nodes by meaning using AI embeddings |
47
- | `naumu_filter` | Filter nodes by type and attributes with deterministic results |
48
- | `naumu_get_node` | Get a node with all its properties and connections |
49
- | `naumu_add_node` | Create a new node |
50
- | `naumu_update_node` | Update an existing node |
51
- | `naumu_add_edge` | Create a relationship between two nodes |
52
- | `naumu_remove_node` | Delete a node and its connections |
53
- | `naumu_remove_edge` | Delete a relationship |
54
- | `naumu_create_topic` | Create a topic (filing destination) in a space; admin-only, name must be a lowercase slug, returns an `id` for `topicIds` params |
55
- | `naumu_ask` | Ask @Naumu a question and get a synthesised answer back (answer + sources + confidence + threadId + status); leaves a visible thread in the space |
56
- | `naumu_delegate` | Hand @Naumu a task to carry out asynchronously (add knowledge, make changes); returns a `threadId` immediately, then poll with `naumu_read_thread` |
57
- | `naumu_read_thread` | Read a thread's messages; each message carries a `status` of `processing` or `complete` |
58
- | `naumu_whoami` | (Bot keys) Self-introspection — id, graphId, name, instructions, allowed tools |
59
- | `naumu_list_threads` | (Bot keys) List threads in your graph with `isParticipant` flag |
60
- | `naumu_get_thread` | (Bot keys) Fetch a thread plus its human + bot participant rosters |
61
- | `naumu_create_thread` | (Bot keys only) Start a new conversation, attach participants, post initial message, optionally file into topics via `topicIds` |
62
- | `naumu_request_attachment_upload` | (Bot keys) Get a presigned S3 URL to upload a file; pair with `naumu_post_message`'s `attachmentIds` |
63
- | `naumu_add_reaction` | (Bot keys) Add an emoji reaction to a message in a thread you participate in (idempotent) |
64
- | `naumu_remove_reaction` | (Bot keys) Remove your own reaction from a message |
65
-
66
- ## Environment variables
97
+ | `naumu_list_graphs` | List every space (graph) the key can access, with ids, names, and roles |
98
+ | `naumu_create_graph` | Create a new, empty space owned by the authenticated user |
99
+ | `naumu_list_members` | List a space's members with their roles |
100
+ | `naumu_whoami` | Return who the calling key is (user, or bot identity with its graph and allowed tools) |
67
101
 
68
- | Variable | Required | Default | Description |
69
- |----------|----------|---------|-------------|
70
- | `NAUMU_API_KEY` | Yes | — | Your API key (starts with `nmu_`) |
71
- | `NAUMU_API_URL` | No | `https://naumu.ai` | Naumu API base URL (only change for self-hosted) |
102
+ ### Schema
103
+
104
+ | Tool | Description |
105
+ |------|-------------|
106
+ | `naumu_get_schema` | Get a space's schema (node types, attributes, connections) |
107
+ | `naumu_update_schema` | Replace the schema with a full new definition |
108
+ | `naumu_add_node_type` | Add one node type without resending the whole schema |
109
+ | `naumu_add_connection` | Add one connection from an existing node type to another |
110
+ | `naumu_add_attribute` | Add or extend an attribute on an existing node type |
111
+ | `naumu_list_schema_violations` | Audit a space against its schema and report violations |
112
+
113
+ ### Reading the graph
114
+
115
+ | Tool | Description |
116
+ |------|-------------|
117
+ | `naumu_search` | Hybrid (meaning plus text) search over graph nodes |
118
+ | `naumu_filter` | Filter nodes by type and attributes, with deterministic and complete results |
119
+ | `naumu_get_node` | Get one node with its properties, edges, notes, threads, and generated context |
120
+ | `naumu_list_node_connections` | List a single node's connections |
121
+ | `naumu_list_dense_nodes` | Find over-connected hub nodes by child count |
122
+ | `naumu_ask` | Ask @Naumu a question and get a synthesised, node-grounded answer with sources and confidence |
123
+
124
+ ### Writing to the graph
125
+
126
+ | Tool | Description |
127
+ |------|-------------|
128
+ | `naumu_add_node` | Create 1-25 nodes in one call |
129
+ | `naumu_update_node` | Overwrite properties of an existing node |
130
+ | `naumu_remove_node` | Delete a node and all of its connections (destructive) |
131
+ | `naumu_add_edge` | Create 1-25 edges between existing nodes |
132
+ | `naumu_remove_edge` | Delete one edge identified by (source, target, label) |
133
+ | `naumu_remove_edges_bulk` | Delete 1-100 edges in one atomic call |
134
+ | `naumu_reparent` | Atomically swap a node's parent edge |
135
+ | `naumu_batch_reparent` | Reparent 1-25 nodes onto a shared new parent |
136
+ | `naumu_delegate` | Hand @Naumu a task to carry out asynchronously; returns a `threadId` to poll |
137
+
138
+ ### Threads and topics
139
+
140
+ | Tool | Description |
141
+ |------|-------------|
142
+ | `naumu_list_threads` | List threads by last activity, with an `isParticipant` flag |
143
+ | `naumu_get_thread` | Fetch one thread plus its human and bot participant rosters |
144
+ | `naumu_read_thread` | Read a thread's messages; each carries `processing` or `complete` status |
145
+ | `naumu_post_message` | Post a markdown message in a thread you participate in |
146
+ | `naumu_create_thread` | **bot** Start a new conversation, attach participants, post a first message, optionally file into topics |
147
+ | `naumu_add_reaction` | Add an emoji reaction to a message (idempotent) |
148
+ | `naumu_remove_reaction` | Remove your own reaction from a message |
149
+ | `naumu_typing` | Show or hide your "is typing" indicator in a thread |
150
+ | `naumu_list_topics` | List a space's topics (filing destinations) for use in `topicIds` |
151
+ | `naumu_create_topic` | Create a topic; admin-only, name must be a lowercase slug |
152
+
153
+ ### Notes
154
+
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `naumu_create_note` | Create a note, optionally with its full markdown body in the same call |
158
+ | `naumu_note_read` | Read a note's current markdown plus the nodes it is tied to |
159
+ | `naumu_note_append` | Append markdown blocks to the end of a note |
160
+ | `naumu_note_insert` | Insert markdown immediately after a named heading |
161
+ | `naumu_note_replace_section` | Replace the body under a named heading |
162
+ | `naumu_note_delete_section` | Delete a heading and its body (destructive) |
163
+ | `naumu_note_replace` | Replace an entire note's content (destructive) |
164
+ | `naumu_note_find_replace` | Literal find and replace within a note |
165
+
166
+ ### Attachments
167
+
168
+ | Tool | Description |
169
+ |------|-------------|
170
+ | `naumu_request_attachment_upload` | Get a presigned S3 URL to upload a file, then reference it via `attachmentIds` |
171
+ | `naumu_get_attachment` | Read a chat attachment by `attachmentId` |
172
+ | `naumu_persist_canvas_attachment` | Promote a canvas-bound pending attachment to a persisted one |
173
+
174
+ ### Space admission
175
+
176
+ | Tool | Description |
177
+ |------|-------------|
178
+ | `naumu_admission_status` | Show who can auto-join a space and who is waiting for approval |
179
+ | `naumu_whitelist_members` | Whitelist emails so those people auto-join on sign-in |
180
+ | `naumu_resolve_admission` | Join (or request to join) the space a repo's `.naumu` file references |
181
+ | `naumu_resolve_join_request` | Approve or deny a pending join request |
182
+
183
+ ### ChatGPT compatibility
184
+
185
+ The OpenAI MCP surface requires tools literally named `search` and `fetch`. These wrap cross-space search and node reads in the response shapes that client expects.
186
+
187
+ | Tool | Description |
188
+ |------|-------------|
189
+ | `search` | Search across every accessible space; returns `{ results: [{ id, title, url }] }` |
190
+ | `fetch` | Fetch a node returned by `search`; returns `{ id, title, text, url }` |
191
+
192
+ ## Troubleshooting
193
+
194
+ When the stdio server is misconfigured it exits before the MCP handshake, and most clients simply show no server at all, with no error. Run the built-in self-check to find out why:
195
+
196
+ ```bash
197
+ npx -y -p @naumu/mcp naumu-mcp doctor
198
+ ```
199
+
200
+ It checks, in order, that `NAUMU_API_KEY` is set (the key itself is never printed), that the API is reachable and accepts the key, and that this build matches the latest published version. It prints one line per check plus a verdict, and exits non-zero if anything failed:
201
+
202
+ ```
203
+ naumu-mcp doctor (v0.11.1, API https://naumu.ai)
204
+
205
+ [fail] API key: NAUMU_API_KEY is missing from the environment
206
+ [warn] API: reachable (https://naumu.ai answered HTTP 403), but there is no NAUMU_API_KEY to verify
207
+ [ok] Version: running 0.11.1 (npm latest)
208
+
209
+ Verdict: 1 check failed - the MCP server will not work until this is fixed.
210
+ Most likely fix: Set NAUMU_API_KEY - create a key at https://naumu.ai/settings (Settings > Your agents > Create an API key)
211
+ ```
212
+
213
+ Run it with the same environment your MCP client uses, otherwise the key check reports on your shell rather than on the client's configuration.
72
214
 
73
215
  ## License
74
216
 
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ function safeErrorMessage(status, upstream) {
30
30
  }
31
31
 
32
32
  // ../mcp-core/src/version.ts
33
- var NAUMU_MCP_VERSION = "0.11.1";
33
+ var NAUMU_MCP_VERSION = "0.12.1";
34
34
 
35
35
  // ../mcp-core/src/client.ts
36
36
  var HEADER_VALUE_MAX_LENGTH = 100;
@@ -677,7 +677,7 @@ function registerGetNode(server2, client2) {
677
677
  {
678
678
  title: "Get Node",
679
679
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
680
- description: "Get a single node with all its properties and connections (incoming and outgoing edges).",
680
+ description: "Get a single node with all its properties, connections (incoming and outgoing edges with neighbor labels), and tied context: attached notes, conversations (threads), scheduled tasks, and the generated summary when one exists.",
681
681
  inputSchema: z11.object({
682
682
  graphId: z11.string().describe("The graph ID"),
683
683
  nodeId: z11.string().describe("The node ID")
@@ -1171,15 +1171,16 @@ function registerPostMessage(server2, client2) {
1171
1171
  idempotentHint: false,
1172
1172
  openWorldHint: false
1173
1173
  },
1174
- 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. Write `content` in markdown (the default format): **bold**, *italic*, `inline code`, fenced code blocks, `- ` bullets, `1. ` ordered lists, and `> ` quotes all render natively; headings and tables are not supported and render as plain text. Mentions are inline pills: `@[Name](id)` mentions a person or bot and `#[label](topic-id)` tags a topic. 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; `@[Naumu](naumu-ai)` addresses the @Naumu agent (a bare `@naumu` in prose also summons it, so only type it when you mean to). @mentioning people loops them in (mention notifications, prompts) without invoking @Naumu. Set contentFormat to "tiptap" only when you need rich content beyond the markdown subset, passing a Tiptap JSON document with mention nodes (`{ type: "mention", attrs: { id, label } }`). 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.',
1174
+ 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. Write `content` in markdown (the default format): **bold**, *italic*, `inline code`, fenced code blocks, `- ` bullets, `1. ` ordered lists, and `> ` quotes all render natively; headings and tables are not supported and render as plain text. Mentions are inline pills: `@[Name](id)` mentions a person or bot and `#[label](topic-id)` tags a topic. 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; `@[Naumu](naumu-ai)` addresses the @Naumu agent (a bare `@naumu` in prose also summons it, so only type it when you mean to). @mentioning people loops them in (mention notifications, prompts) without invoking @Naumu. Set contentFormat to "tiptap" only when you need rich content beyond the markdown subset, passing a Tiptap JSON document with mention nodes (`{ type: "mention", attrs: { id, label } }`). 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. By default this only appends text: @Naumu is NOT summoned unless the thread auto-responds or you mention it, so nothing is committed to the graph. Pass `invokeAgent: true` when you need @Naumu to act on the message (record a work-log entry, file a status update); it summons @Naumu unconditionally, even in paused threads, and @Naumu works in the background - poll naumu_read_thread to see what it did. To get a synthesised answer from @Naumu, use naumu_ask.',
1175
1175
  inputSchema: z20.object({
1176
1176
  threadId: z20.string().describe("The thread ID to post into. You must be a participant in this thread."),
1177
1177
  content: z20.string().optional().describe('Message body. Markdown by default (see the tool description for the supported subset and the `@[Name](id)` mention pill syntax); a Tiptap JSON document when contentFormat is "tiptap". Optional when `attachmentIds` is provided.'),
1178
1178
  contentFormat: z20.enum(["tiptap", "markdown"]).optional().describe('Format of `content`. Defaults to "markdown" (rendered subset plus `@[Name](id)` / `#[label](topic-id)` mention pills). Use "tiptap" for full rich content, e.g. a doc containing `{ type: "mention", attrs: { id: userIdOrEmailOrIdentityId, label: displayName } }`.'),
1179
- 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.")
1179
+ 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."),
1180
+ invokeAgent: z20.boolean().optional().describe("Set true to summon @Naumu on this message unconditionally (mention-equivalent), even in threads where auto-response is paused. Required whenever the post must be committed to the graph (work-log entries, status updates). @Naumu processes in the background; poll naumu_read_thread. Defaults to false: plain append, no agent turn.")
1180
1181
  })
1181
1182
  },
1182
- async ({ threadId, content, contentFormat, attachmentIds }) => {
1183
+ async ({ threadId, content, contentFormat, attachmentIds, invokeAgent }) => {
1183
1184
  try {
1184
1185
  const body = {
1185
1186
  // 'markdown' is the announced name for the API's 'text' mode;
@@ -1188,6 +1189,10 @@ function registerPostMessage(server2, client2) {
1188
1189
  };
1189
1190
  if (content !== void 0) body.content = content;
1190
1191
  if (attachmentIds && attachmentIds.length > 0) body.attachmentIds = attachmentIds;
1192
+ if (invokeAgent === true) {
1193
+ body.invokeAgent = true;
1194
+ body.async = true;
1195
+ }
1191
1196
  const data = await client2.post(`/api/threads/${threadId}/messages`, body);
1192
1197
  return {
1193
1198
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
@@ -1283,15 +1288,31 @@ function registerListThreads(server2, client2) {
1283
1288
  {
1284
1289
  title: "List Threads",
1285
1290
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1286
- 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.",
1291
+ 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). Pass `nodeId` alongside `graphId` to list only the conversations tied to that node (attached, or that created/modified it). 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.",
1287
1292
  inputSchema: z23.object({
1288
1293
  graphId: z23.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1294
+ nodeId: z23.string().optional().describe("Scope the listing to conversations tied to this node. Requires `graphId`."),
1289
1295
  cursor: z23.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1290
1296
  limit: z23.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1291
1297
  })
1292
1298
  },
1293
- async ({ graphId, cursor, limit }) => {
1299
+ async ({ graphId, nodeId, cursor, limit }) => {
1294
1300
  try {
1301
+ if (nodeId) {
1302
+ if (!graphId) {
1303
+ return {
1304
+ content: [{ type: "text", text: "Error: `nodeId` requires `graphId`." }],
1305
+ isError: true
1306
+ };
1307
+ }
1308
+ const data2 = await client2.get(
1309
+ `/api/threads?graphId=${encodeURIComponent(graphId)}&nodeId=${encodeURIComponent(nodeId)}`
1310
+ );
1311
+ const clean2 = Array.isArray(data2) ? data2.map(sanitizeThreadParticipants) : data2;
1312
+ return {
1313
+ content: [{ type: "text", text: JSON.stringify(clean2, null, 2) }]
1314
+ };
1315
+ }
1295
1316
  const params = new URLSearchParams();
1296
1317
  if (cursor !== void 0) params.set("cursor", String(cursor));
1297
1318
  if (limit !== void 0) params.set("limit", String(limit));
@@ -1841,7 +1862,7 @@ function registerNoteRead(server2, client2) {
1841
1862
  {
1842
1863
  title: "Read Note",
1843
1864
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1844
- description: "Read the current contents of a note as markdown; use before editing so you know what you're working with. `naumu_note_find_replace` and the section-based tools (`naumu_note_insert`, `naumu_note_replace_section`, `naumu_note_delete_section`) anchor on text/headings present in the live doc.",
1865
+ description: "Read the current contents of a note as markdown, with its title and `connections` (the graph nodes the note is tied to); 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.",
1845
1866
  inputSchema: z34.object({
1846
1867
  noteId: z34.string().describe("The note (Thought) ID")
1847
1868
  })
@@ -2345,7 +2366,8 @@ var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2345
2366
  "updatedAt",
2346
2367
  "heatConvSignal",
2347
2368
  "heatConvComputedAt",
2348
- "heatComputedAt"
2369
+ "heatComputedAt",
2370
+ "context"
2349
2371
  ]);
2350
2372
  function parseResourceId(id) {
2351
2373
  const sep = id.indexOf(":");
@@ -2374,14 +2396,42 @@ ${attrLines.join("\n")}`);
2374
2396
  const edgeLines = [];
2375
2397
  for (const e of connections?.outgoing ?? []) {
2376
2398
  if (!e?.target) continue;
2377
- edgeLines.push(`- ${e.label ?? "related"} \u2192 ${e.target}${e.isParent ? " (parent)" : ""}`);
2399
+ const target = e.targetLabel ? `${e.targetLabel} (${e.target})` : e.target;
2400
+ edgeLines.push(`- ${e.label ?? "related"} \u2192 ${target}${e.isParent ? " (parent)" : ""}`);
2378
2401
  }
2379
2402
  for (const e of connections?.incoming ?? []) {
2380
2403
  if (!e?.source) continue;
2381
- edgeLines.push(`- ${e.source} \u2192 ${e.label ?? "related"} (incoming)`);
2404
+ const source = e.sourceLabel ? `${e.sourceLabel} (${e.source})` : e.source;
2405
+ edgeLines.push(`- ${source} \u2192 ${e.label ?? "related"} (incoming)`);
2382
2406
  }
2383
2407
  if (edgeLines.length) parts.push(`Connections:
2384
2408
  ${edgeLines.join("\n")}`);
2409
+ const ctx = node.context ?? {};
2410
+ if (ctx.notes?.length) {
2411
+ const more = (ctx.notesTotal ?? ctx.notes.length) - ctx.notes.length;
2412
+ const lines = ctx.notes.map((n) => `- ${n.title || "Untitled"} (${n.id})`);
2413
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2414
+ parts.push(`Notes:
2415
+ ${lines.join("\n")}`);
2416
+ }
2417
+ if (ctx.threads?.length) {
2418
+ const more = (ctx.threadsTotal ?? ctx.threads.length) - ctx.threads.length;
2419
+ const lines = ctx.threads.map(
2420
+ (t) => `- ${t.title || "Untitled"} (${t.id}${t.relations?.length ? ` \xB7 ${t.relations.join(", ")}` : ""})`
2421
+ );
2422
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2423
+ parts.push(`Conversations:
2424
+ ${lines.join("\n")}`);
2425
+ }
2426
+ if (ctx.scheduledTasks?.length) {
2427
+ const lines = ctx.scheduledTasks.map(
2428
+ (t) => `- ${t.title} (${t.id}${t.cron ? ` \xB7 cron ${t.cron}` : ""})`
2429
+ );
2430
+ parts.push(`Scheduled tasks:
2431
+ ${lines.join("\n")}`);
2432
+ }
2433
+ if (ctx.summary?.content) parts.push(`Summary:
2434
+ ${ctx.summary.content}`);
2385
2435
  return parts.join("\n\n");
2386
2436
  }
2387
2437
  function registerChatgptFetch(server2, client2) {
@@ -2642,11 +2692,12 @@ var TOOL_REGISTRARS = {
2642
2692
  naumu_whoami: registerWhoami,
2643
2693
  naumu_list_threads: registerListThreads,
2644
2694
  naumu_list_topics: registerListTopics,
2645
- // User-surface only. `POST /api/graphs/:id/topics` requires the ADMIN-only
2646
- // `space:manage-topics` permission, and bot identities always resolve to the
2647
- // editor role, so a bot calling this could only ever 403. It is therefore
2648
- // omitted from BOT_ONLY_TOOL_NAMES and has no entry in the backend
2649
- // PERMISSION_TO_MCP_TOOLS map (same treatment as the admission tools below).
2695
+ // User-surface only. Omitted from BOT_ONLY_TOOL_NAMES and from the backend
2696
+ // PERMISSION_TO_MCP_TOOLS map (same treatment as the admission tools
2697
+ // below), so bot manifests never include it. Note the backend gate alone
2698
+ // no longer keeps bots out: `space:manage-topics` is editor+ since
2699
+ // 2026-08-11 and bot identities resolve to the editor role — the manifest
2700
+ // omission is what keeps this tool off the bot surface.
2650
2701
  naumu_create_topic: registerCreateTopic,
2651
2702
  naumu_get_thread: registerGetThread,
2652
2703
  naumu_create_thread: registerCreateThread,
@@ -2715,11 +2766,204 @@ function registerNamedTools(server2, client2, toolNames) {
2715
2766
  console.error(`[mcp] registered ${registered.length} tool(s) for bot key`);
2716
2767
  }
2717
2768
 
2769
+ // src/doctor.ts
2770
+ var MARKERS = {
2771
+ pass: "[ok] ",
2772
+ fail: "[fail]",
2773
+ warn: "[warn]"
2774
+ };
2775
+ var BOT_KEY_PREFIX = "nmu_bot_";
2776
+ var SETTINGS_URL = "https://naumu.ai/settings";
2777
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@naumu/mcp/latest";
2778
+ var API_TIMEOUT_MS = 1e4;
2779
+ var REGISTRY_TIMEOUT_MS = 4e3;
2780
+ var errorMessage = (err) => err instanceof Error ? err.message : String(err);
2781
+ var timeoutSignal = (ms) => typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
2782
+ var formatDoctorCheck = (check) => `${MARKERS[check.status]} ${check.label}: ${check.detail}`;
2783
+ var checkApiKey = (apiKey2) => {
2784
+ if (!apiKey2) {
2785
+ return {
2786
+ label: "API key",
2787
+ status: "fail",
2788
+ detail: "NAUMU_API_KEY is missing from the environment",
2789
+ fix: `Set NAUMU_API_KEY - create a key at ${SETTINGS_URL} (Settings > Your agents > Create an API key)`
2790
+ };
2791
+ }
2792
+ const kind = apiKey2.startsWith(BOT_KEY_PREFIX) ? "bot key" : "user key";
2793
+ return {
2794
+ label: "API key",
2795
+ status: "pass",
2796
+ detail: `NAUMU_API_KEY is set (${kind}; value not printed)`
2797
+ };
2798
+ };
2799
+ var describeWhoami = (data) => {
2800
+ const row = data ?? {};
2801
+ if (row.kind === "user") {
2802
+ const email = row.email ? ` <${row.email}>` : "";
2803
+ return `authenticated as ${row.name ?? "unknown user"}${email}`;
2804
+ }
2805
+ if (row.id) {
2806
+ return `authenticated as bot identity ${row.name ?? row.id} (graph ${row.graphId ?? "unknown"})`;
2807
+ }
2808
+ return "authenticated, but the identity payload was not recognized";
2809
+ };
2810
+ var checkApiReachability = async (options) => {
2811
+ const label = "API";
2812
+ const doFetch = options.fetchImpl ?? fetch;
2813
+ const headers = { Accept: "application/json" };
2814
+ if (options.apiKey) headers.Authorization = `Bearer ${options.apiKey}`;
2815
+ let res;
2816
+ try {
2817
+ res = await doFetch(`${options.apiUrl}/api/identities/me/whoami`, {
2818
+ headers,
2819
+ signal: timeoutSignal(options.timeoutMs ?? API_TIMEOUT_MS)
2820
+ });
2821
+ } catch (err) {
2822
+ return {
2823
+ label,
2824
+ status: "fail",
2825
+ detail: `unreachable - no response from ${options.apiUrl} (${errorMessage(err)})`,
2826
+ fix: `Check your network and NAUMU_API_URL - nothing answered at ${options.apiUrl}`
2827
+ };
2828
+ }
2829
+ if (res.status === 401 || res.status === 403) {
2830
+ if (!options.apiKey) {
2831
+ return {
2832
+ label,
2833
+ status: "warn",
2834
+ detail: `reachable (${options.apiUrl} answered HTTP ${res.status}), but there is no NAUMU_API_KEY to verify`
2835
+ };
2836
+ }
2837
+ return {
2838
+ label,
2839
+ status: "fail",
2840
+ detail: `reachable, but ${options.apiUrl} rejected the key (HTTP ${res.status})`,
2841
+ fix: `NAUMU_API_KEY is invalid or revoked - create a new key at ${SETTINGS_URL} and update your MCP config`
2842
+ };
2843
+ }
2844
+ if (!res.ok) {
2845
+ return {
2846
+ label,
2847
+ status: "fail",
2848
+ detail: `reachable, but ${options.apiUrl} answered HTTP ${res.status}`,
2849
+ fix: "The Naumu API returned an unexpected status - retry shortly, or check NAUMU_API_URL points at the front-end origin"
2850
+ };
2851
+ }
2852
+ try {
2853
+ const data = await res.json();
2854
+ return { label, status: "pass", detail: `reachable at ${options.apiUrl}; ${describeWhoami(data)}` };
2855
+ } catch (err) {
2856
+ return {
2857
+ label,
2858
+ status: "fail",
2859
+ detail: `reachable, but the response from ${options.apiUrl} was not JSON (${errorMessage(err)})`,
2860
+ fix: "NAUMU_API_URL does not look like a Naumu API - point it at https://naumu.ai"
2861
+ };
2862
+ }
2863
+ };
2864
+ var compareVersions = (a, b) => {
2865
+ const parse = (value) => value.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
2866
+ const left = parse(a);
2867
+ const right = parse(b);
2868
+ for (let i = 0; i < 3; i += 1) {
2869
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
2870
+ if (diff !== 0) return diff < 0 ? -1 : 1;
2871
+ }
2872
+ return 0;
2873
+ };
2874
+ var checkVersionFreshness = async (options = {}) => {
2875
+ const label = "Version";
2876
+ const current = options.currentVersion ?? NAUMU_MCP_VERSION;
2877
+ const doFetch = options.fetchImpl ?? fetch;
2878
+ let latest;
2879
+ try {
2880
+ const res = await doFetch(REGISTRY_LATEST_URL, {
2881
+ headers: { Accept: "application/json" },
2882
+ signal: timeoutSignal(options.timeoutMs ?? REGISTRY_TIMEOUT_MS)
2883
+ });
2884
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2885
+ const body = await res.json();
2886
+ latest = body?.version;
2887
+ } catch (err) {
2888
+ return {
2889
+ label,
2890
+ status: "warn",
2891
+ detail: `running ${current}; could not check the npm registry for the latest version (${errorMessage(err)})`
2892
+ };
2893
+ }
2894
+ if (!latest) {
2895
+ return {
2896
+ label,
2897
+ status: "warn",
2898
+ detail: `running ${current}; could not check the npm registry for the latest version (no version field in the response)`
2899
+ };
2900
+ }
2901
+ const comparison = compareVersions(current, latest);
2902
+ if (comparison < 0) {
2903
+ return {
2904
+ label,
2905
+ status: "fail",
2906
+ detail: `running ${current}, but npm latest is ${latest}`,
2907
+ fix: 'Update @naumu/mcp - run "npm cache clean --force" and reinstall (globally installed: "npm i -g @naumu/mcp@latest"; npx users: pin "@naumu/mcp@latest")'
2908
+ };
2909
+ }
2910
+ if (comparison > 0) {
2911
+ return { label, status: "pass", detail: `running ${current}, ahead of npm latest ${latest} (unpublished build)` };
2912
+ }
2913
+ return { label, status: "pass", detail: `running ${current} (npm latest)` };
2914
+ };
2915
+ var runDoctorChecks = async (options) => {
2916
+ const checks = [];
2917
+ checks.push(checkApiKey(options.apiKey));
2918
+ checks.push(
2919
+ await checkApiReachability({
2920
+ apiUrl: options.apiUrl,
2921
+ apiKey: options.apiKey,
2922
+ fetchImpl: options.fetchImpl
2923
+ })
2924
+ );
2925
+ checks.push(
2926
+ await checkVersionFreshness({
2927
+ currentVersion: options.currentVersion,
2928
+ fetchImpl: options.fetchImpl
2929
+ })
2930
+ );
2931
+ return checks;
2932
+ };
2933
+ var renderDoctorReport = (checks, context) => {
2934
+ const version = context.version ?? NAUMU_MCP_VERSION;
2935
+ const lines = [
2936
+ `naumu-mcp doctor (v${version}, API ${context.apiUrl})`,
2937
+ "",
2938
+ ...checks.map(formatDoctorCheck),
2939
+ ""
2940
+ ];
2941
+ const failures = checks.filter((check) => check.status === "fail");
2942
+ if (failures.length === 0) {
2943
+ lines.push("Verdict: all checks passed - the MCP server should start normally.");
2944
+ return { text: lines.join("\n"), exitCode: 0 };
2945
+ }
2946
+ const plural = failures.length === 1 ? "check" : "checks";
2947
+ lines.push(`Verdict: ${failures.length} ${plural} failed - the MCP server will not work until this is fixed.`);
2948
+ const fix = failures.find((check) => check.fix)?.fix;
2949
+ if (fix) lines.push(`Most likely fix: ${fix}`);
2950
+ return { text: lines.join("\n"), exitCode: 1 };
2951
+ };
2952
+ var runDoctorCli = async (options) => {
2953
+ const checks = await runDoctorChecks({ apiUrl: options.apiUrl, apiKey: options.apiKey });
2954
+ const report = renderDoctorReport(checks, { apiUrl: options.apiUrl });
2955
+ console.log(report.text);
2956
+ return report.exitCode;
2957
+ };
2958
+
2718
2959
  // src/index.ts
2719
2960
  var DEFAULT_API_URL = "https://naumu.ai";
2720
- var BOT_KEY_PREFIX = "nmu_bot_";
2961
+ var BOT_KEY_PREFIX2 = "nmu_bot_";
2721
2962
  var apiUrl = (process.env.NAUMU_API_URL || DEFAULT_API_URL).replace(/\/api\/?$/, "");
2722
2963
  var apiKey = process.env.NAUMU_API_KEY;
2964
+ if (process.argv[2] === "doctor") {
2965
+ process.exit(await runDoctorCli({ apiUrl, apiKey }));
2966
+ }
2723
2967
  if (!apiKey) {
2724
2968
  console.error("Missing required environment variable: NAUMU_API_KEY");
2725
2969
  process.exit(1);
@@ -2734,7 +2978,7 @@ var server = new McpServer(
2734
2978
  instructions: NAUMU_INSTRUCTIONS
2735
2979
  }
2736
2980
  );
2737
- var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX);
2981
+ var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX2);
2738
2982
  if (isBotKey) {
2739
2983
  try {
2740
2984
  const manifest = await client.get("/api/identities/me/mcp-tool-manifest");
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@naumu/mcp",
3
- "version": "0.11.1",
4
- "description": "MCP server for Naumu access your knowledge graph from Claude Code, Cursor, and other AI coding agents",
3
+ "version": "0.12.1",
4
+ "description": "MCP server for Naumu - access your knowledge graph from Claude Code, Cursor, and other AI coding agents",
5
5
  "license": "MIT",
6
6
  "author": "Naumu <hello@naumu.ai>",
7
+ "mcpName": "ai.naumu/mcp",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/naumu-app/naumu",
10
- "directory": "packages/mcp"
10
+ "url": "https://github.com/naumu-ai/mcp"
11
11
  },
12
12
  "homepage": "https://naumu.ai",
13
- "bugs": "https://github.com/naumu-app/naumu/issues",
13
+ "bugs": "https://github.com/naumu-ai/mcp/issues",
14
14
  "keywords": [
15
15
  "mcp",
16
16
  "model-context-protocol",
@@ -28,7 +28,8 @@
28
28
  "type": "module",
29
29
  "files": [
30
30
  "dist",
31
- "README.md"
31
+ "README.md",
32
+ "server.json"
32
33
  ],
33
34
  "engines": {
34
35
  "node": ">=18"
package/server.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "ai.naumu/mcp",
4
+ "title": "Naumu",
5
+ "description": "Search, extend, and act on your team's Naumu knowledge graph: notes, threads, and nodes.",
6
+ "version": "0.12.1",
7
+ "websiteUrl": "https://naumu.ai",
8
+ "repository": {
9
+ "url": "https://github.com/naumu-ai/mcp",
10
+ "source": "github"
11
+ },
12
+ "icons": [
13
+ {
14
+ "src": "https://cdn.naumu.ai/brand/icon-256.png",
15
+ "mimeType": "image/png",
16
+ "sizes": ["256x256"]
17
+ }
18
+ ],
19
+ "remotes": [
20
+ {
21
+ "type": "streamable-http",
22
+ "url": "https://naumu.ai/api/mcp"
23
+ }
24
+ ],
25
+ "packages": [
26
+ {
27
+ "registryType": "npm",
28
+ "registryBaseUrl": "https://registry.npmjs.org",
29
+ "identifier": "@naumu/mcp",
30
+ "version": "0.12.1",
31
+ "runtimeHint": "npx",
32
+ "transport": { "type": "stdio" },
33
+ "environmentVariables": [
34
+ {
35
+ "name": "NAUMU_API_KEY",
36
+ "description": "Naumu API key (starts with nmu_), created in Naumu under Settings > Your agents.",
37
+ "isRequired": true,
38
+ "isSecret": true,
39
+ "format": "string"
40
+ },
41
+ {
42
+ "name": "NAUMU_API_URL",
43
+ "description": "Override the Naumu API base URL. Defaults to https://naumu.ai.",
44
+ "isRequired": false,
45
+ "isSecret": false,
46
+ "format": "string"
47
+ },
48
+ {
49
+ "name": "NAUMU_IDENTITY_ID",
50
+ "description": "Optional bot identity id to act as when the key covers several identities.",
51
+ "isRequired": false,
52
+ "isSecret": false,
53
+ "format": "string"
54
+ }
55
+ ]
56
+ }
57
+ ]
58
+ }