@naumu/mcp 0.11.1 → 0.12.0

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.0";
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")
@@ -1283,15 +1283,31 @@ function registerListThreads(server2, client2) {
1283
1283
  {
1284
1284
  title: "List Threads",
1285
1285
  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.",
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). 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
1287
  inputSchema: z23.object({
1288
1288
  graphId: z23.string().optional().describe("Graph (space) ID. Required for user API keys; omit for bot identity keys (defaults to your own graph)."),
1289
+ nodeId: z23.string().optional().describe("Scope the listing to conversations tied to this node. Requires `graphId`."),
1289
1290
  cursor: z23.number().int().optional().describe("Unix timestamp ms \u2014 returns threads with `lastActivityAt` strictly older than this. Omit for the newest page."),
1290
1291
  limit: z23.number().int().min(1).max(200).optional().describe("Page size, default 50, max 200.")
1291
1292
  })
1292
1293
  },
1293
- async ({ graphId, cursor, limit }) => {
1294
+ async ({ graphId, nodeId, cursor, limit }) => {
1294
1295
  try {
1296
+ if (nodeId) {
1297
+ if (!graphId) {
1298
+ return {
1299
+ content: [{ type: "text", text: "Error: `nodeId` requires `graphId`." }],
1300
+ isError: true
1301
+ };
1302
+ }
1303
+ const data2 = await client2.get(
1304
+ `/api/threads?graphId=${encodeURIComponent(graphId)}&nodeId=${encodeURIComponent(nodeId)}`
1305
+ );
1306
+ const clean2 = Array.isArray(data2) ? data2.map(sanitizeThreadParticipants) : data2;
1307
+ return {
1308
+ content: [{ type: "text", text: JSON.stringify(clean2, null, 2) }]
1309
+ };
1310
+ }
1295
1311
  const params = new URLSearchParams();
1296
1312
  if (cursor !== void 0) params.set("cursor", String(cursor));
1297
1313
  if (limit !== void 0) params.set("limit", String(limit));
@@ -1841,7 +1857,7 @@ function registerNoteRead(server2, client2) {
1841
1857
  {
1842
1858
  title: "Read Note",
1843
1859
  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.",
1860
+ 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
1861
  inputSchema: z34.object({
1846
1862
  noteId: z34.string().describe("The note (Thought) ID")
1847
1863
  })
@@ -2345,7 +2361,8 @@ var NON_ATTRIBUTE_PROPS = /* @__PURE__ */ new Set([
2345
2361
  "updatedAt",
2346
2362
  "heatConvSignal",
2347
2363
  "heatConvComputedAt",
2348
- "heatComputedAt"
2364
+ "heatComputedAt",
2365
+ "context"
2349
2366
  ]);
2350
2367
  function parseResourceId(id) {
2351
2368
  const sep = id.indexOf(":");
@@ -2374,14 +2391,42 @@ ${attrLines.join("\n")}`);
2374
2391
  const edgeLines = [];
2375
2392
  for (const e of connections?.outgoing ?? []) {
2376
2393
  if (!e?.target) continue;
2377
- edgeLines.push(`- ${e.label ?? "related"} \u2192 ${e.target}${e.isParent ? " (parent)" : ""}`);
2394
+ const target = e.targetLabel ? `${e.targetLabel} (${e.target})` : e.target;
2395
+ edgeLines.push(`- ${e.label ?? "related"} \u2192 ${target}${e.isParent ? " (parent)" : ""}`);
2378
2396
  }
2379
2397
  for (const e of connections?.incoming ?? []) {
2380
2398
  if (!e?.source) continue;
2381
- edgeLines.push(`- ${e.source} \u2192 ${e.label ?? "related"} (incoming)`);
2399
+ const source = e.sourceLabel ? `${e.sourceLabel} (${e.source})` : e.source;
2400
+ edgeLines.push(`- ${source} \u2192 ${e.label ?? "related"} (incoming)`);
2382
2401
  }
2383
2402
  if (edgeLines.length) parts.push(`Connections:
2384
2403
  ${edgeLines.join("\n")}`);
2404
+ const ctx = node.context ?? {};
2405
+ if (ctx.notes?.length) {
2406
+ const more = (ctx.notesTotal ?? ctx.notes.length) - ctx.notes.length;
2407
+ const lines = ctx.notes.map((n) => `- ${n.title || "Untitled"} (${n.id})`);
2408
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2409
+ parts.push(`Notes:
2410
+ ${lines.join("\n")}`);
2411
+ }
2412
+ if (ctx.threads?.length) {
2413
+ const more = (ctx.threadsTotal ?? ctx.threads.length) - ctx.threads.length;
2414
+ const lines = ctx.threads.map(
2415
+ (t) => `- ${t.title || "Untitled"} (${t.id}${t.relations?.length ? ` \xB7 ${t.relations.join(", ")}` : ""})`
2416
+ );
2417
+ if (more > 0) lines.push(`- \u2026and ${more} more`);
2418
+ parts.push(`Conversations:
2419
+ ${lines.join("\n")}`);
2420
+ }
2421
+ if (ctx.scheduledTasks?.length) {
2422
+ const lines = ctx.scheduledTasks.map(
2423
+ (t) => `- ${t.title} (${t.id}${t.cron ? ` \xB7 cron ${t.cron}` : ""})`
2424
+ );
2425
+ parts.push(`Scheduled tasks:
2426
+ ${lines.join("\n")}`);
2427
+ }
2428
+ if (ctx.summary?.content) parts.push(`Summary:
2429
+ ${ctx.summary.content}`);
2385
2430
  return parts.join("\n\n");
2386
2431
  }
2387
2432
  function registerChatgptFetch(server2, client2) {
@@ -2642,11 +2687,12 @@ var TOOL_REGISTRARS = {
2642
2687
  naumu_whoami: registerWhoami,
2643
2688
  naumu_list_threads: registerListThreads,
2644
2689
  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).
2690
+ // User-surface only. Omitted from BOT_ONLY_TOOL_NAMES and from the backend
2691
+ // PERMISSION_TO_MCP_TOOLS map (same treatment as the admission tools
2692
+ // below), so bot manifests never include it. Note the backend gate alone
2693
+ // no longer keeps bots out: `space:manage-topics` is editor+ since
2694
+ // 2026-08-11 and bot identities resolve to the editor role — the manifest
2695
+ // omission is what keeps this tool off the bot surface.
2650
2696
  naumu_create_topic: registerCreateTopic,
2651
2697
  naumu_get_thread: registerGetThread,
2652
2698
  naumu_create_thread: registerCreateThread,
@@ -2715,11 +2761,204 @@ function registerNamedTools(server2, client2, toolNames) {
2715
2761
  console.error(`[mcp] registered ${registered.length} tool(s) for bot key`);
2716
2762
  }
2717
2763
 
2764
+ // src/doctor.ts
2765
+ var MARKERS = {
2766
+ pass: "[ok] ",
2767
+ fail: "[fail]",
2768
+ warn: "[warn]"
2769
+ };
2770
+ var BOT_KEY_PREFIX = "nmu_bot_";
2771
+ var SETTINGS_URL = "https://naumu.ai/settings";
2772
+ var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@naumu/mcp/latest";
2773
+ var API_TIMEOUT_MS = 1e4;
2774
+ var REGISTRY_TIMEOUT_MS = 4e3;
2775
+ var errorMessage = (err) => err instanceof Error ? err.message : String(err);
2776
+ var timeoutSignal = (ms) => typeof AbortSignal?.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
2777
+ var formatDoctorCheck = (check) => `${MARKERS[check.status]} ${check.label}: ${check.detail}`;
2778
+ var checkApiKey = (apiKey2) => {
2779
+ if (!apiKey2) {
2780
+ return {
2781
+ label: "API key",
2782
+ status: "fail",
2783
+ detail: "NAUMU_API_KEY is missing from the environment",
2784
+ fix: `Set NAUMU_API_KEY - create a key at ${SETTINGS_URL} (Settings > Your agents > Create an API key)`
2785
+ };
2786
+ }
2787
+ const kind = apiKey2.startsWith(BOT_KEY_PREFIX) ? "bot key" : "user key";
2788
+ return {
2789
+ label: "API key",
2790
+ status: "pass",
2791
+ detail: `NAUMU_API_KEY is set (${kind}; value not printed)`
2792
+ };
2793
+ };
2794
+ var describeWhoami = (data) => {
2795
+ const row = data ?? {};
2796
+ if (row.kind === "user") {
2797
+ const email = row.email ? ` <${row.email}>` : "";
2798
+ return `authenticated as ${row.name ?? "unknown user"}${email}`;
2799
+ }
2800
+ if (row.id) {
2801
+ return `authenticated as bot identity ${row.name ?? row.id} (graph ${row.graphId ?? "unknown"})`;
2802
+ }
2803
+ return "authenticated, but the identity payload was not recognized";
2804
+ };
2805
+ var checkApiReachability = async (options) => {
2806
+ const label = "API";
2807
+ const doFetch = options.fetchImpl ?? fetch;
2808
+ const headers = { Accept: "application/json" };
2809
+ if (options.apiKey) headers.Authorization = `Bearer ${options.apiKey}`;
2810
+ let res;
2811
+ try {
2812
+ res = await doFetch(`${options.apiUrl}/api/identities/me/whoami`, {
2813
+ headers,
2814
+ signal: timeoutSignal(options.timeoutMs ?? API_TIMEOUT_MS)
2815
+ });
2816
+ } catch (err) {
2817
+ return {
2818
+ label,
2819
+ status: "fail",
2820
+ detail: `unreachable - no response from ${options.apiUrl} (${errorMessage(err)})`,
2821
+ fix: `Check your network and NAUMU_API_URL - nothing answered at ${options.apiUrl}`
2822
+ };
2823
+ }
2824
+ if (res.status === 401 || res.status === 403) {
2825
+ if (!options.apiKey) {
2826
+ return {
2827
+ label,
2828
+ status: "warn",
2829
+ detail: `reachable (${options.apiUrl} answered HTTP ${res.status}), but there is no NAUMU_API_KEY to verify`
2830
+ };
2831
+ }
2832
+ return {
2833
+ label,
2834
+ status: "fail",
2835
+ detail: `reachable, but ${options.apiUrl} rejected the key (HTTP ${res.status})`,
2836
+ fix: `NAUMU_API_KEY is invalid or revoked - create a new key at ${SETTINGS_URL} and update your MCP config`
2837
+ };
2838
+ }
2839
+ if (!res.ok) {
2840
+ return {
2841
+ label,
2842
+ status: "fail",
2843
+ detail: `reachable, but ${options.apiUrl} answered HTTP ${res.status}`,
2844
+ fix: "The Naumu API returned an unexpected status - retry shortly, or check NAUMU_API_URL points at the front-end origin"
2845
+ };
2846
+ }
2847
+ try {
2848
+ const data = await res.json();
2849
+ return { label, status: "pass", detail: `reachable at ${options.apiUrl}; ${describeWhoami(data)}` };
2850
+ } catch (err) {
2851
+ return {
2852
+ label,
2853
+ status: "fail",
2854
+ detail: `reachable, but the response from ${options.apiUrl} was not JSON (${errorMessage(err)})`,
2855
+ fix: "NAUMU_API_URL does not look like a Naumu API - point it at https://naumu.ai"
2856
+ };
2857
+ }
2858
+ };
2859
+ var compareVersions = (a, b) => {
2860
+ const parse = (value) => value.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
2861
+ const left = parse(a);
2862
+ const right = parse(b);
2863
+ for (let i = 0; i < 3; i += 1) {
2864
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
2865
+ if (diff !== 0) return diff < 0 ? -1 : 1;
2866
+ }
2867
+ return 0;
2868
+ };
2869
+ var checkVersionFreshness = async (options = {}) => {
2870
+ const label = "Version";
2871
+ const current = options.currentVersion ?? NAUMU_MCP_VERSION;
2872
+ const doFetch = options.fetchImpl ?? fetch;
2873
+ let latest;
2874
+ try {
2875
+ const res = await doFetch(REGISTRY_LATEST_URL, {
2876
+ headers: { Accept: "application/json" },
2877
+ signal: timeoutSignal(options.timeoutMs ?? REGISTRY_TIMEOUT_MS)
2878
+ });
2879
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2880
+ const body = await res.json();
2881
+ latest = body?.version;
2882
+ } catch (err) {
2883
+ return {
2884
+ label,
2885
+ status: "warn",
2886
+ detail: `running ${current}; could not check the npm registry for the latest version (${errorMessage(err)})`
2887
+ };
2888
+ }
2889
+ if (!latest) {
2890
+ return {
2891
+ label,
2892
+ status: "warn",
2893
+ detail: `running ${current}; could not check the npm registry for the latest version (no version field in the response)`
2894
+ };
2895
+ }
2896
+ const comparison = compareVersions(current, latest);
2897
+ if (comparison < 0) {
2898
+ return {
2899
+ label,
2900
+ status: "fail",
2901
+ detail: `running ${current}, but npm latest is ${latest}`,
2902
+ 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")'
2903
+ };
2904
+ }
2905
+ if (comparison > 0) {
2906
+ return { label, status: "pass", detail: `running ${current}, ahead of npm latest ${latest} (unpublished build)` };
2907
+ }
2908
+ return { label, status: "pass", detail: `running ${current} (npm latest)` };
2909
+ };
2910
+ var runDoctorChecks = async (options) => {
2911
+ const checks = [];
2912
+ checks.push(checkApiKey(options.apiKey));
2913
+ checks.push(
2914
+ await checkApiReachability({
2915
+ apiUrl: options.apiUrl,
2916
+ apiKey: options.apiKey,
2917
+ fetchImpl: options.fetchImpl
2918
+ })
2919
+ );
2920
+ checks.push(
2921
+ await checkVersionFreshness({
2922
+ currentVersion: options.currentVersion,
2923
+ fetchImpl: options.fetchImpl
2924
+ })
2925
+ );
2926
+ return checks;
2927
+ };
2928
+ var renderDoctorReport = (checks, context) => {
2929
+ const version = context.version ?? NAUMU_MCP_VERSION;
2930
+ const lines = [
2931
+ `naumu-mcp doctor (v${version}, API ${context.apiUrl})`,
2932
+ "",
2933
+ ...checks.map(formatDoctorCheck),
2934
+ ""
2935
+ ];
2936
+ const failures = checks.filter((check) => check.status === "fail");
2937
+ if (failures.length === 0) {
2938
+ lines.push("Verdict: all checks passed - the MCP server should start normally.");
2939
+ return { text: lines.join("\n"), exitCode: 0 };
2940
+ }
2941
+ const plural = failures.length === 1 ? "check" : "checks";
2942
+ lines.push(`Verdict: ${failures.length} ${plural} failed - the MCP server will not work until this is fixed.`);
2943
+ const fix = failures.find((check) => check.fix)?.fix;
2944
+ if (fix) lines.push(`Most likely fix: ${fix}`);
2945
+ return { text: lines.join("\n"), exitCode: 1 };
2946
+ };
2947
+ var runDoctorCli = async (options) => {
2948
+ const checks = await runDoctorChecks({ apiUrl: options.apiUrl, apiKey: options.apiKey });
2949
+ const report = renderDoctorReport(checks, { apiUrl: options.apiUrl });
2950
+ console.log(report.text);
2951
+ return report.exitCode;
2952
+ };
2953
+
2718
2954
  // src/index.ts
2719
2955
  var DEFAULT_API_URL = "https://naumu.ai";
2720
- var BOT_KEY_PREFIX = "nmu_bot_";
2956
+ var BOT_KEY_PREFIX2 = "nmu_bot_";
2721
2957
  var apiUrl = (process.env.NAUMU_API_URL || DEFAULT_API_URL).replace(/\/api\/?$/, "");
2722
2958
  var apiKey = process.env.NAUMU_API_KEY;
2959
+ if (process.argv[2] === "doctor") {
2960
+ process.exit(await runDoctorCli({ apiUrl, apiKey }));
2961
+ }
2723
2962
  if (!apiKey) {
2724
2963
  console.error("Missing required environment variable: NAUMU_API_KEY");
2725
2964
  process.exit(1);
@@ -2734,7 +2973,7 @@ var server = new McpServer(
2734
2973
  instructions: NAUMU_INSTRUCTIONS
2735
2974
  }
2736
2975
  );
2737
- var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX);
2976
+ var isBotKey = apiKey.startsWith(BOT_KEY_PREFIX2);
2738
2977
  if (isBotKey) {
2739
2978
  try {
2740
2979
  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.0",
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.0",
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.0",
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
+ }