@nexusbloom/mcp-server 1.0.3 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @nexusbloom/mcp-server
2
+
3
+ MCP (Model Context Protocol) server for [NexusBloom](https://nexusbloom.dev).
4
+
5
+ An agent connects once and can then **discover tools by what it wants to do**,
6
+ read their exact parameters, and run them. No local runtime, no credentials
7
+ beyond the API, no state on disk.
8
+
9
+ Built on [`@nexusbloom/core`](https://www.npmjs.com/package/@nexusbloom/core) —
10
+ the same engine the CLI, the VS Code extension and the embed use, so a type rule
11
+ or validation change lands once and every surface inherits it.
12
+
13
+ ## Install
14
+
15
+ Add it to any MCP client that speaks stdio:
16
+
17
+ ```jsonc
18
+ {
19
+ "mcpServers": {
20
+ "nexusbloom": {
21
+ "command": "npx",
22
+ "args": ["-y", "@nexusbloom/mcp-server"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ Or run it directly:
29
+
30
+ ```bash
31
+ npx @nexusbloom/mcp-server
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ All variables are optional.
37
+
38
+ | Variable | Default | Purpose |
39
+ |---|---|---|
40
+ | `NEXUSBLOOM_API_KEY` | *(none)* | API key. Without it the server runs anonymously under the free-tier rate limit. |
41
+ | `NEXUSBLOOM_API_URL` | `https://nexusbloom.dev/api` | API base. Accepts the bare host or the full `/api` form. |
42
+ | `NEXUSBLOOM_MCP_TIMEOUT_MS` | `15000` | Per-request timeout. |
43
+ | `NEXUSBLOOM_MCP_CACHE_TTL_MS` | `60000` | Tool-list cache lifetime. `0` disables caching. |
44
+ | `NEXUSBLOOM_MCP_DEBUG` | *(off)* | `1` logs requests to stderr. |
45
+
46
+ Diagnostics always go to **stderr**; stdout is the MCP transport and writing
47
+ anything else there corrupts the protocol.
48
+
49
+ ## What an agent sees
50
+
51
+ Every published tool is advertised as an MCP tool **named by its slug**, with its
52
+ real JSON Schema — so it can be called directly:
53
+
54
+ ```jsonc
55
+ { "name": "env-validator", "arguments": { "env_content": "DEBUG=true" } }
56
+ ```
57
+
58
+ Alongside it is a `nexusbloom` meta-tool for when the agent does not yet know
59
+ which tool it wants:
60
+
61
+ | Command | What it does |
62
+ |---|---|
63
+ | `{"command":"search","query":"validate env file"}` | Rank tools by intent. Works on prose, not just slug fragments. |
64
+ | `{"command":"list"}` | Every published tool, one line each. |
65
+ | `{"command":"schema","slug":"…"}` | Exact parameters, plus a ready-to-send example invocation. |
66
+ | `{"command":"run","slug":"…","params":{…}}` | Execute a tool. |
67
+
68
+ ### Errors are actionable
69
+
70
+ Failures come back as an `isError` result with a stable `code` and a
71
+ `retryable` flag, so an agent can decide whether to retry rather than parsing
72
+ prose:
73
+
74
+ ```jsonc
75
+ {
76
+ "success": false,
77
+ "error": "No tool named \"env-validatr\" is published. Closest matches: env-validator. …",
78
+ "code": "TOOL_NOT_FOUND",
79
+ "retryable": false
80
+ }
81
+ ```
82
+
83
+ A wrong slug suggests the right one. A missing argument names the field. A
84
+ timeout is marked retryable; a validation failure is not.
85
+
86
+ ### Validation before the request
87
+
88
+ Input is checked against the tool's own schema before anything is sent, so a
89
+ malformed call costs no API quota and returns an error naming the exact field.
90
+ The API remains authoritative — this only rejects what is provably wrong.
91
+
92
+ ## Design notes
93
+
94
+ **Execution never guesses.** Slugs resolve by exact match or unambiguous
95
+ abbreviation only. Asking for `env-validatr` is an error with a suggestion, not
96
+ a silent run of `env-validator` — a wrong-but-successful result is worse than a
97
+ wrong-but-obvious failure.
98
+
99
+ **Search is lexical, not embedded.** Ranking runs with no network, no model and
100
+ no vector store, is deterministic, and returns identical output for identical
101
+ input. That makes it testable and fast enough to run on every call. It matches
102
+ across slug, name, description, tags and input field names, with light
103
+ morphological handling so "environment" reaches `env-validator`.
104
+
105
+ **Failures degrade, they do not crash.** An unreachable API produces a working
106
+ server that explains itself on each request, not a process that exits or a host
107
+ that concludes the server is broken. A failed catalogue refresh keeps serving the
108
+ last good list rather than emptying it.
109
+
110
+
package/index.js CHANGED
@@ -1,422 +1,84 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  /**
4
3
  * @nexusbloom/mcp-server
5
4
  *
6
- * MCP (Model Context Protocol) server for NexusBloom.
7
- * AI agents use a single tool to discover, inspect, and execute
8
- * any tool in the NexusBloom ecosystem.
9
- *
10
- * No Supabase credentials needed. Connects to the NexusBloom API.
5
+ * MCP server for NexusBloom. Agents discover tools by intent, read their exact
6
+ * parameters, and execute them — no credentials beyond the API, no local
7
+ * runtime, no state.
11
8
  *
12
9
  * Usage:
13
10
  * npx @nexusbloom/mcp-server
14
11
  *
15
- * Environment variables (all optional):
16
- * NEXUSBLOOM_API_KEY — API key for authenticated tool execution
17
- * NEXUSBLOOM_API_URL — API base URL (default: https://nexusbloom.dev/api)
12
+ * Environment (all optional):
13
+ * NEXUSBLOOM_API_KEY API key for authenticated execution
14
+ * NEXUSBLOOM_API_URL API base (default: https://nexusbloom.dev/api)
15
+ * NEXUSBLOOM_MCP_TIMEOUT_MS Request timeout in ms (default: 15000)
16
+ * NEXUSBLOOM_MCP_CACHE_TTL_MS Tool-list cache TTL in ms (default: 60000)
17
+ * NEXUSBLOOM_MCP_DEBUG "1" to log requests to stderr
18
+ *
19
+ * stdout is the MCP transport. All diagnostics go to stderr.
18
20
  */
19
21
 
20
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
- import {
23
- CallToolRequestSchema,
24
- ListToolsRequestSchema,
25
- } from "@modelcontextprotocol/sdk/types.js";
26
-
27
- // ---------------------------------------------------------------------------
28
- // Configuration
29
- // ---------------------------------------------------------------------------
30
23
 
31
- const API_KEY = process.env.NEXUSBLOOM_API_KEY || "";
32
- const API_URL = (process.env.NEXUSBLOOM_API_URL || "https://nexusbloom.dev/api").replace(/\/+$/, "");
33
-
34
- // Optional: if the API URL is a local dev server, allow insecure HTTPS
35
- const FETCH_OPTS = API_URL.includes("localhost") || API_URL.includes("127.0.0.1")
36
- ? {}
37
- : {};
38
-
39
- function apiHeaders() {
40
- const headers = { "Content-Type": "application/json" };
41
- if (API_KEY) headers["Authorization"] = `Bearer ${API_KEY}`;
42
- return headers;
24
+ import { ApiClient } from "./src/client.js";
25
+ import { candidateRoots, loadConfig } from "./src/config.js";
26
+ import { checkConnectivity } from "./src/handlers.js";
27
+ import { ManifestCache } from "./src/manifests.js";
28
+ import { createServer } from "./src/server.js";
29
+
30
+ /** Compose every piece from a config, for both `main()` and tests. */
31
+ export function createApp(config, deps = {}) {
32
+ const client = new ApiClient(config, deps);
33
+ const cache = new ManifestCache(client, { ttlMs: config.cacheTtlMs, now: deps.now });
34
+ const { server, handlers } = createServer({ client, cache, config });
35
+ return { client, cache, server, handlers, config };
43
36
  }
44
37
 
45
- // ---------------------------------------------------------------------------
46
- // Tool manifest cache
47
- // ---------------------------------------------------------------------------
48
-
49
- let toolsCache = null;
50
- let toolsCacheTime = 0;
51
- const CACHE_TTL = 30_000; // 30 seconds
52
-
53
- async function fetchTools() {
54
- const now = Date.now();
55
- if (toolsCache && now - toolsCacheTime < CACHE_TTL) return toolsCache;
56
-
57
- try {
58
- const res = await fetch(`${API_URL}/tools`, {
59
- headers: apiHeaders(),
60
- ...FETCH_OPTS,
61
- });
62
- if (!res.ok) {
63
- console.error(`API returned ${res.status} for /tools`);
64
- return toolsCache || [];
65
- }
66
- const json = await res.json();
67
- const tools = (json.tools || json.data || json || []).filter(Boolean);
68
-
69
- toolsCache = tools.map(t => ({
70
- slug: t.slug,
71
- name: t.name,
72
- short_description: t.short_description || t.description || "",
73
- tags: (t.tags || []).flat().filter(Boolean),
74
- category: t.category || "Platform Tools",
75
- icon: t.icon || "🔧",
76
- version: t.version || "1.0.0",
77
- type: "builtin",
78
- input_schema: t.input_schema || null,
79
- }));
80
- toolsCacheTime = now;
81
- return toolsCache;
82
- } catch (err) {
83
- console.error("Failed to fetch tools:", err.message);
84
- return toolsCache || [];
85
- }
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Fetch a single tool's schema
90
- // ---------------------------------------------------------------------------
91
-
92
- async function fetchToolSchema(slug) {
93
- // Primary: fetch manifest from /api/run/{slug} (the reliable endpoint)
94
- try {
95
- const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
96
- headers: apiHeaders(),
97
- ...FETCH_OPTS,
98
- });
99
- if (res.ok) {
100
- // API returns { success: true, data: { manifest: { ... } } }
101
- const body = await res.json();
102
- const manifest = body.data?.manifest || body.manifest || body;
103
- if (manifest && manifest.slug) {
104
- return {
105
- slug: manifest.slug,
106
- name: manifest.name,
107
- description: manifest.short_description || "",
108
- tags: manifest.tags || [],
109
- input_schema: manifest.input_schema || {},
110
- output_schema: manifest.output_schema || {},
111
- icon: manifest.icon || "🔧",
112
- version: manifest.version || "1.0.0",
113
- category: manifest.category || "Platform Tools",
114
- };
115
- }
116
- }
117
- } catch (err) {
118
- console.error(`Failed to fetch manifest for ${slug}:`, err.message);
119
- }
120
-
121
- return null;
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // Execute a tool
126
- // ---------------------------------------------------------------------------
127
-
128
- async function executeTool(slug, params) {
129
- const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
130
- method: "POST",
131
- headers: apiHeaders(),
132
- body: JSON.stringify(params),
133
- ...FETCH_OPTS,
134
- });
135
-
136
- if (!res.ok) {
137
- const err = await res.json().catch(() => ({ error: res.statusText }));
138
- throw new Error(err.error || `API returned ${res.status}`);
139
- }
140
-
141
- const json = await res.json();
142
- return {
143
- success: true,
144
- data: json.data || json,
145
- tool: slug,
146
- execution: "live",
147
- };
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // MCP Server
152
- // ---------------------------------------------------------------------------
153
-
154
- const server = new Server(
155
- {
156
- name: "nexusbloom-mcp",
157
- version: "1.0.0",
158
- },
159
- {
160
- capabilities: {
161
- tools: {},
162
- },
163
- },
164
- );
165
-
166
- // ---- ListTools ----
167
- server.setRequestHandler(ListToolsRequestSchema, async () => {
168
- const tools = await fetchTools();
169
-
170
- const toolList = tools.map((t) => ({
171
- name: t.slug,
172
- description: `${t.name}: ${t.short_description || "No description"}${
173
- t.category ? ` [${t.category}]` : ""
174
- }`,
175
- inputSchema: t.input_schema || {
176
- type: "object",
177
- properties: {},
178
- },
179
- }));
180
-
181
- // Also expose the meta-tool for list/schema/run
182
- toolList.push({
183
- name: "nexusbloom",
184
- description: `Meta-tool to list, inspect, or run any NexusBloom tool. Use this when you need to discover tools or get schema details.`,
185
- inputSchema: {
186
- type: "object",
187
- properties: {
188
- command: {
189
- type: "string",
190
- enum: ["list", "schema", "run"],
191
- description:
192
- "What to do: 'list' returns all available tools, 'schema' returns a tool's full input/output schema, 'run' executes a tool",
193
- },
194
- slug: {
195
- type: "string",
196
- description:
197
- "Tool slug (required for 'schema' and 'run' commands). Use 'list' first to see available slugs.",
198
- },
199
- params: {
200
- type: "object",
201
- description:
202
- "Input parameters matching the tool's input_schema (required for 'run' command). Use 'schema' first to see the expected format.",
203
- },
204
- },
205
- required: ["command"],
206
- },
207
- });
208
-
209
- return { tools: toolList };
210
- });
211
-
212
- // ---- CallTool ----
213
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
214
- const { name: toolName, arguments: args } = request.params;
215
-
216
- // ── Direct tool execution by slug ──────────────────────────────
217
- // AI agents can call any tool directly by its slug name.
218
- // The tool's own input_schema defines the expected parameters.
219
- // We verify the slug exists by checking against our tool list.
220
- if (toolName !== "nexusbloom") {
221
- const tools = await fetchTools();
222
- const matched = tools.find((t) => t.slug === toolName);
223
- if (!matched) {
224
- return {
225
- content: [{ type: "text", text: `Unknown tool: "${toolName}". Use the "nexusbloom" tool with command "list" to see available tools.` }],
226
- isError: true,
227
- };
228
- }
229
- try {
230
- const result = await executeTool(toolName, args || {});
231
- const formatted = JSON.stringify(result.data, null, 2);
232
- return {
233
- content: [
234
- { type: "text", text: `# ${matched.name} (${toolName})\n\n\`\`\`json\n${formatted}\n\`\`\`` },
235
- { type: "resource", resource: { text: formatted, uri: "data:application/json,", mimeType: "application/json" } },
236
- ],
237
- };
238
- } catch (err) {
239
- return { content: [{ type: "text", text: `Error executing "${toolName}": ${err.message}` }], isError: true };
240
- }
241
- }
242
-
243
- // ── Legacy meta-tool: list/schema/run via sub-commands ─────────
244
- const command = args?.command;
245
-
246
- if (!command || !["list", "schema", "run"].includes(command)) {
247
- return {
248
- content: [
249
- {
250
- type: "text",
251
- text: "Invalid command. Use one of: 'list', 'schema', 'run'.",
252
- },
253
- ],
254
- isError: true,
255
- };
256
- }
257
-
258
- // ---- COMMAND: list ----
259
- if (command === "list") {
260
- const tools = await fetchTools();
261
-
262
- const summary = tools
263
- .map((t) => `• ${t.icon} **${t.name}** (\`${t.slug}\`) — ${t.short_description || "No description"} [${t.category}]`)
264
- .join("\n");
265
-
266
- return {
267
- content: [
268
- {
269
- type: "text",
270
- text: `# NexusBloom Tools\n\nAvailable tools: ${tools.length}\n\n${summary}\n\n---\nTo inspect a tool's schema, call with \`command: "schema"\` and its \`slug\`.\nTo run a tool, call with \`command: "run"\`, its \`slug\`, and matching \`params\`.`,
271
- },
272
- {
273
- type: "resource",
274
- resource: {
275
- text: JSON.stringify(tools, null, 2),
276
- uri: "data:application/json,",
277
- mimeType: "application/json",
278
- },
279
- },
280
- ],
281
- };
282
- }
283
-
284
- // ---- COMMAND: schema ----
285
- if (command === "schema") {
286
- const slug = args?.slug;
287
- if (!slug) {
288
- return {
289
- content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
290
- isError: true,
291
- };
292
- }
293
-
294
- const schema = await fetchToolSchema(slug);
295
- if (!schema) {
296
- return {
297
- content: [{ type: "text", text: `Tool "${slug}" not found. Use 'list' to see available tools.` }],
298
- isError: true,
299
- };
300
- }
301
-
302
- const inputProps = schema.input_schema?.properties || {};
303
- const requiredFields = schema.input_schema?.required || [];
304
- const paramDocs = Object.entries(inputProps)
305
- .map(([key, prop]) => {
306
- const req = requiredFields.includes(key) ? " (required)" : "";
307
- const def = prop.default !== undefined ? ` (default: ${JSON.stringify(prop.default)})` : "";
308
- const enum_ = prop.enum ? ` [${prop.enum.join(", ")}]` : "";
309
- return ` • \`${key}\`: ${prop.type || "any"}${req}${def}${enum_} — ${prop.description || ""}`;
310
- })
311
- .join("\n");
312
-
313
- return {
314
- content: [
315
- {
316
- type: "text",
317
- text: `# ${schema.icon} ${schema.name}\n\n**Slug:** \`${schema.slug}\`\n**Description:** ${schema.description}\n**Category:** ${schema.category || "Uncategorized"}\n**Version:** ${schema.version}\n\n## Input Parameters\n${paramDocs || " _(no parameters)_"}\n\nTo run this tool, call with \`command: "run"\`, \`slug: "${schema.slug}"\`, and \`params\` matching the above schema.`,
318
- },
319
- {
320
- type: "resource",
321
- resource: {
322
- text: JSON.stringify(schema, null, 2),
323
- uri: "data:application/json,",
324
- mimeType: "application/json",
325
- },
326
- },
327
- ],
328
- };
329
- }
38
+ /**
39
+ * Start the server.
40
+ *
41
+ * @returns {Promise<{close: () => Promise<void>}>}
42
+ */
43
+ export async function main(env = process.env, deps = {}) {
44
+ const config = loadConfig(env);
45
+ const app = createApp(config, deps);
330
46
 
331
- // ---- COMMAND: run ----
332
- if (command === "run") {
333
- const slug = args?.slug;
334
- const params = args?.params || {};
47
+ // Probe before serving. A failure here is reported and then ignored: the
48
+ // server still starts, because a transient DNS blip should not require
49
+ // restarting an agent host.
50
+ const connectivity = await checkConnectivity(app.client, candidateRoots(env), config, deps);
51
+ process.stderr.write(`${connectivity.report}\n`);
335
52
 
336
- if (!slug) {
337
- return {
338
- content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
339
- isError: true,
340
- };
341
- }
53
+ const transport = new StdioServerTransport();
342
54
 
55
+ // Close on the signals a host actually sends. Without this the process
56
+ // lingers after the parent exits and leaves orphan MCP servers behind.
57
+ const shutdown = async () => {
343
58
  try {
344
- const result = await executeTool(slug, params);
345
- return {
346
- content: [
347
- {
348
- type: "text",
349
- text: `# Tool Result: ${slug}\n\n**Execution:** ${result.execution}\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\``,
350
- },
351
- {
352
- type: "resource",
353
- resource: {
354
- text: JSON.stringify(result, null, 2),
355
- uri: "data:application/json,",
356
- mimeType: "application/json",
357
- },
358
- },
359
- ],
360
- };
361
- } catch (err) {
362
- return {
363
- content: [{ type: "text", text: `Error executing "${slug}": ${err.message}` }],
364
- isError: true,
365
- };
59
+ await app.server.close();
60
+ } catch {
61
+ /* already closing */
366
62
  }
367
- }
368
- });
63
+ app.client.abortAll();
64
+ };
65
+ process.once("SIGINT", shutdown);
66
+ process.once("SIGTERM", shutdown);
369
67
 
370
- // ---- Start ----
371
- async function main() {
372
- try {
373
- // Verify API connectivity — try multiple possible API URLs
374
- const apiUrlCandidates = [
375
- API_URL,
376
- API_URL.replace("/api", ""), // base domain without /api
377
- ];
68
+ await app.server.connect(transport);
69
+ process.stderr.write("NexusBloom MCP server running on stdio\n");
378
70
 
379
- let connected = false;
380
- for (const url of apiUrlCandidates) {
381
- try {
382
- const testUrl = url.includes("/api") ? `${url}/tools` : `${url}/api/tools`;
383
- const res = await fetch(testUrl, { ...FETCH_OPTS });
384
- if (res.ok || res.status === 429) {
385
- console.error(`NexusBloom MCP server connected to ${url}`);
386
- connected = true;
387
- break;
388
- } else if (res.status === 404) {
389
- // Try without /tools path
390
- const res2 = await fetch(url, { ...FETCH_OPTS });
391
- if (res2.ok) {
392
- console.error(`NexusBloom MCP server connected to ${url}`);
393
- connected = true;
394
- break;
395
- }
396
- }
397
- } catch (_) {
398
- // Continue to next candidate
399
- }
400
- }
71
+ return { ...app, transport, close: shutdown };
72
+ }
401
73
 
402
- if (!connected) {
403
- console.error(
404
- `WARNING: Could not reach NexusBloom API. ` +
405
- `The MCP server will start in degraded mode — tool execution requires a working API.\n` +
406
- `Checked endpoints:\n` +
407
- apiUrlCandidates.map(u => ` - ${u.includes("/api") ? `${u}/tools` : `${u}/api/tools`}`).join("\n") + "\n" +
408
- `To fix: ensure NEXUSBLOOM_API_URL is set correctly (default: https://nexusbloom.dev/api)\n` +
409
- `Or deploy the platform API and verify the Vercel rewrite from your frontend project.`
410
- );
411
- }
74
+ // Only run when executed directly, so importing this module in a test does not
75
+ // hijack stdio.
76
+ const invokedDirectly =
77
+ process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
412
78
 
413
- const transport = new StdioServerTransport();
414
- await server.connect(transport);
415
- console.error("NexusBloom MCP server running on stdio");
416
- } catch (err) {
417
- console.error("Fatal error:", err);
79
+ if (invokedDirectly) {
80
+ main().catch((err) => {
81
+ process.stderr.write(`Fatal error starting MCP server: ${err?.stack || err}\n`);
418
82
  process.exit(1);
419
- }
83
+ });
420
84
  }
421
-
422
- main();
package/package.json CHANGED
@@ -1,16 +1,43 @@
1
1
  {
2
2
  "name": "@nexusbloom/mcp-server",
3
- "version": "1.0.3",
4
- "description": "MCP server for NexusBloom — AI agents discover and execute tools through a single interface. No Supabase credentials needed, just connect to our API.",
3
+ "version": "2.0.2",
4
+ "description": "MCP server for NexusBloom — agents discover tools by intent, read exact schemas, and execute them. Built on @nexusbloom/core.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "nexusbloom-mcp": "index.js"
9
9
  },
10
+ "files": [
11
+ "index.js",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
10
18
  "engines": {
11
19
  "node": ">=18"
12
20
  },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "ai",
25
+ "agents",
26
+ "tools",
27
+ "nexusbloom"
28
+ ],
13
29
  "dependencies": {
14
- "@modelcontextprotocol/sdk": "^1.8.0"
30
+ "@modelcontextprotocol/sdk": "^1.8.0",
31
+ "@nexusbloom/core": "^1.0.0"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test --import ./test/setup.mjs test/*.test.js",
35
+ "test:coverage": "node --test --experimental-test-coverage --import ./test/setup.mjs test/*.test.js",
36
+ "test:watch": "node --test --watch --import ./test/setup.mjs test/*.test.js",
37
+ "test:src-only": "node --test --import ./test/setup.mjs test/config.test.js test/errors.test.js test/manifests.test.js test/discovery.test.js test/client.test.js test/validate.test.js test/render.test.js test/cache.test.js test/handlers.test.js",
38
+ "shell": "node scripts/mcp-shell.mjs",
39
+ "shell:mock": "node scripts/mcp-shell.mjs --mock",
40
+ "mock-api": "node scripts/mock-api.mjs",
41
+ "lint": "node --check index.js && for f in src/*.js; do node --check \"$f\" || exit 1; done"
15
42
  }
16
43
  }