@nexusbloom/mcp-server 1.0.2 → 2.0.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 +125 -0
- package/index.js +59 -399
- package/package.json +27 -3
- package/src/client.js +156 -0
- package/src/config.js +117 -0
- package/src/discovery.js +326 -0
- package/src/errors.js +107 -0
- package/src/handlers.js +426 -0
- package/src/manifests.js +247 -0
- package/src/render.js +348 -0
- package/src/server.js +83 -0
- package/src/validate.js +104 -0
- package/test-mcp-client.mjs +0 -104
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm test # unit + integration, no network
|
|
114
|
+
npm run test:coverage # with coverage
|
|
115
|
+
npm run lint
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The suite has **417 tests** at **100% line coverage** of `src/`. It includes
|
|
119
|
+
end-to-end tests that drive the real server as a child process over stdio, using
|
|
120
|
+
scripted miniature agents that discover, choose, read a schema and execute — the
|
|
121
|
+
same loop a model runs, asserted on whether the *task* succeeded.
|
|
122
|
+
|
|
123
|
+
No test touches the network: `NEXUSBLOOM_API_URL` is pointed at an unroutable
|
|
124
|
+
`.invalid` host and every HTTP client is injected, so the suite cannot pass
|
|
125
|
+
while production is broken, and cannot fail because production is down.
|
package/index.js
CHANGED
|
@@ -1,424 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* @nexusbloom/mcp-server
|
|
5
4
|
*
|
|
6
|
-
* MCP
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
16
|
-
* NEXUSBLOOM_API_KEY
|
|
17
|
-
* NEXUSBLOOM_API_URL
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
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
|
-
price_type: t.price_type || t.pricing || "free",
|
|
77
|
-
version: t.version || "1.0.0",
|
|
78
|
-
type: "builtin",
|
|
79
|
-
input_schema: t.input_schema || null,
|
|
80
|
-
}));
|
|
81
|
-
toolsCacheTime = now;
|
|
82
|
-
return toolsCache;
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error("Failed to fetch tools:", err.message);
|
|
85
|
-
return toolsCache || [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Fetch a single tool's schema
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
async function fetchToolSchema(slug) {
|
|
94
|
-
// Primary: fetch manifest from /api/run/{slug} (the reliable endpoint)
|
|
95
|
-
try {
|
|
96
|
-
const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
|
|
97
|
-
headers: apiHeaders(),
|
|
98
|
-
...FETCH_OPTS,
|
|
99
|
-
});
|
|
100
|
-
if (res.ok) {
|
|
101
|
-
// API returns { success: true, data: { manifest: { ... } } }
|
|
102
|
-
const body = await res.json();
|
|
103
|
-
const manifest = body.data?.manifest || body.manifest || body;
|
|
104
|
-
if (manifest && manifest.slug) {
|
|
105
|
-
return {
|
|
106
|
-
slug: manifest.slug,
|
|
107
|
-
name: manifest.name,
|
|
108
|
-
description: manifest.short_description || "",
|
|
109
|
-
tags: manifest.tags || [],
|
|
110
|
-
input_schema: manifest.input_schema || {},
|
|
111
|
-
output_schema: manifest.output_schema || {},
|
|
112
|
-
icon: manifest.icon || "🔧",
|
|
113
|
-
price_type: manifest.pricing || "free",
|
|
114
|
-
version: manifest.version || "1.0.0",
|
|
115
|
-
category: manifest.category || "Platform Tools",
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error(`Failed to fetch manifest for ${slug}:`, err.message);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Execute a tool
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
async function executeTool(slug, params) {
|
|
131
|
-
const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
|
|
132
|
-
method: "POST",
|
|
133
|
-
headers: apiHeaders(),
|
|
134
|
-
body: JSON.stringify(params),
|
|
135
|
-
...FETCH_OPTS,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!res.ok) {
|
|
139
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
140
|
-
throw new Error(err.error || `API returned ${res.status}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const json = await res.json();
|
|
144
|
-
return {
|
|
145
|
-
success: true,
|
|
146
|
-
data: json.data || json,
|
|
147
|
-
tool: slug,
|
|
148
|
-
execution: "live",
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// MCP Server
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
const server = new Server(
|
|
157
|
-
{
|
|
158
|
-
name: "nexusbloom-mcp",
|
|
159
|
-
version: "1.0.0",
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
capabilities: {
|
|
163
|
-
tools: {},
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// ---- ListTools ----
|
|
169
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
170
|
-
const tools = await fetchTools();
|
|
171
|
-
|
|
172
|
-
const toolList = tools.map((t) => ({
|
|
173
|
-
name: t.slug,
|
|
174
|
-
description: `${t.name}: ${t.short_description || "No description"}${
|
|
175
|
-
t.category ? ` [${t.category}]` : ""
|
|
176
|
-
}`,
|
|
177
|
-
inputSchema: t.input_schema || {
|
|
178
|
-
type: "object",
|
|
179
|
-
properties: {},
|
|
180
|
-
},
|
|
181
|
-
}));
|
|
182
|
-
|
|
183
|
-
// Also expose the meta-tool for list/schema/run
|
|
184
|
-
toolList.push({
|
|
185
|
-
name: "nexusbloom",
|
|
186
|
-
description: `Meta-tool to list, inspect, or run any NexusBloom tool. Use this when you need to discover tools or get schema details.`,
|
|
187
|
-
inputSchema: {
|
|
188
|
-
type: "object",
|
|
189
|
-
properties: {
|
|
190
|
-
command: {
|
|
191
|
-
type: "string",
|
|
192
|
-
enum: ["list", "schema", "run"],
|
|
193
|
-
description:
|
|
194
|
-
"What to do: 'list' returns all available tools, 'schema' returns a tool's full input/output schema, 'run' executes a tool",
|
|
195
|
-
},
|
|
196
|
-
slug: {
|
|
197
|
-
type: "string",
|
|
198
|
-
description:
|
|
199
|
-
"Tool slug (required for 'schema' and 'run' commands). Use 'list' first to see available slugs.",
|
|
200
|
-
},
|
|
201
|
-
params: {
|
|
202
|
-
type: "object",
|
|
203
|
-
description:
|
|
204
|
-
"Input parameters matching the tool's input_schema (required for 'run' command). Use 'schema' first to see the expected format.",
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
required: ["command"],
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
return { tools: toolList };
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ---- CallTool ----
|
|
215
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
216
|
-
const { name: toolName, arguments: args } = request.params;
|
|
217
|
-
|
|
218
|
-
// ── Direct tool execution by slug ──────────────────────────────
|
|
219
|
-
// AI agents can call any tool directly by its slug name.
|
|
220
|
-
// The tool's own input_schema defines the expected parameters.
|
|
221
|
-
// We verify the slug exists by checking against our tool list.
|
|
222
|
-
if (toolName !== "nexusbloom") {
|
|
223
|
-
const tools = await fetchTools();
|
|
224
|
-
const matched = tools.find((t) => t.slug === toolName);
|
|
225
|
-
if (!matched) {
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text", text: `Unknown tool: "${toolName}". Use the "nexusbloom" tool with command "list" to see available tools.` }],
|
|
228
|
-
isError: true,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
const result = await executeTool(toolName, args || {});
|
|
233
|
-
const formatted = JSON.stringify(result.data, null, 2);
|
|
234
|
-
return {
|
|
235
|
-
content: [
|
|
236
|
-
{ type: "text", text: `# ${matched.name} (${toolName})\n\n\`\`\`json\n${formatted}\n\`\`\`` },
|
|
237
|
-
{ type: "resource", resource: { text: formatted, uri: "data:application/json,", mimeType: "application/json" } },
|
|
238
|
-
],
|
|
239
|
-
};
|
|
240
|
-
} catch (err) {
|
|
241
|
-
return { content: [{ type: "text", text: `Error executing "${toolName}": ${err.message}` }], isError: true };
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── Legacy meta-tool: list/schema/run via sub-commands ─────────
|
|
246
|
-
const command = args?.command;
|
|
247
|
-
|
|
248
|
-
if (!command || !["list", "schema", "run"].includes(command)) {
|
|
249
|
-
return {
|
|
250
|
-
content: [
|
|
251
|
-
{
|
|
252
|
-
type: "text",
|
|
253
|
-
text: "Invalid command. Use one of: 'list', 'schema', 'run'.",
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
isError: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ---- COMMAND: list ----
|
|
261
|
-
if (command === "list") {
|
|
262
|
-
const tools = await fetchTools();
|
|
263
|
-
|
|
264
|
-
const summary = tools
|
|
265
|
-
.map((t) => `• ${t.icon} **${t.name}** (\`${t.slug}\`) — ${t.short_description || "No description"} [${t.category}]`)
|
|
266
|
-
.join("\n");
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
content: [
|
|
270
|
-
{
|
|
271
|
-
type: "text",
|
|
272
|
-
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\`.`,
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
type: "resource",
|
|
276
|
-
resource: {
|
|
277
|
-
text: JSON.stringify(tools, null, 2),
|
|
278
|
-
uri: "data:application/json,",
|
|
279
|
-
mimeType: "application/json",
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// ---- COMMAND: schema ----
|
|
287
|
-
if (command === "schema") {
|
|
288
|
-
const slug = args?.slug;
|
|
289
|
-
if (!slug) {
|
|
290
|
-
return {
|
|
291
|
-
content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
|
|
292
|
-
isError: true,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const schema = await fetchToolSchema(slug);
|
|
297
|
-
if (!schema) {
|
|
298
|
-
return {
|
|
299
|
-
content: [{ type: "text", text: `Tool "${slug}" not found. Use 'list' to see available tools.` }],
|
|
300
|
-
isError: true,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const inputProps = schema.input_schema?.properties || {};
|
|
305
|
-
const requiredFields = schema.input_schema?.required || [];
|
|
306
|
-
const paramDocs = Object.entries(inputProps)
|
|
307
|
-
.map(([key, prop]) => {
|
|
308
|
-
const req = requiredFields.includes(key) ? " (required)" : "";
|
|
309
|
-
const def = prop.default !== undefined ? ` (default: ${JSON.stringify(prop.default)})` : "";
|
|
310
|
-
const enum_ = prop.enum ? ` [${prop.enum.join(", ")}]` : "";
|
|
311
|
-
return ` • \`${key}\`: ${prop.type || "any"}${req}${def}${enum_} — ${prop.description || ""}`;
|
|
312
|
-
})
|
|
313
|
-
.join("\n");
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
content: [
|
|
317
|
-
{
|
|
318
|
-
type: "text",
|
|
319
|
-
text: `# ${schema.icon} ${schema.name}\n\n**Slug:** \`${schema.slug}\`\n**Description:** ${schema.description}\n**Category:** ${schema.category || "Uncategorized"}\n**Price:** ${schema.price_type}\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.`,
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
type: "resource",
|
|
323
|
-
resource: {
|
|
324
|
-
text: JSON.stringify(schema, null, 2),
|
|
325
|
-
uri: "data:application/json,",
|
|
326
|
-
mimeType: "application/json",
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
};
|
|
331
|
-
}
|
|
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);
|
|
332
46
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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`);
|
|
337
52
|
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
|
|
341
|
-
isError: true,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
53
|
+
const transport = new StdioServerTransport();
|
|
344
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 () => {
|
|
345
58
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
{
|
|
350
|
-
type: "text",
|
|
351
|
-
text: `# Tool Result: ${slug}\n\n**Execution:** ${result.execution}\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\``,
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
type: "resource",
|
|
355
|
-
resource: {
|
|
356
|
-
text: JSON.stringify(result, null, 2),
|
|
357
|
-
uri: "data:application/json,",
|
|
358
|
-
mimeType: "application/json",
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
};
|
|
363
|
-
} catch (err) {
|
|
364
|
-
return {
|
|
365
|
-
content: [{ type: "text", text: `Error executing "${slug}": ${err.message}` }],
|
|
366
|
-
isError: true,
|
|
367
|
-
};
|
|
59
|
+
await app.server.close();
|
|
60
|
+
} catch {
|
|
61
|
+
/* already closing */
|
|
368
62
|
}
|
|
369
|
-
|
|
370
|
-
}
|
|
63
|
+
app.client.abortAll();
|
|
64
|
+
};
|
|
65
|
+
process.once("SIGINT", shutdown);
|
|
66
|
+
process.once("SIGTERM", shutdown);
|
|
371
67
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
// Verify API connectivity — try multiple possible API URLs
|
|
376
|
-
const apiUrlCandidates = [
|
|
377
|
-
API_URL,
|
|
378
|
-
API_URL.replace("/api", ""), // base domain without /api
|
|
379
|
-
];
|
|
68
|
+
await app.server.connect(transport);
|
|
69
|
+
process.stderr.write("NexusBloom MCP server running on stdio\n");
|
|
380
70
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
const testUrl = url.includes("/api") ? `${url}/tools` : `${url}/api/tools`;
|
|
385
|
-
const res = await fetch(testUrl, { ...FETCH_OPTS });
|
|
386
|
-
if (res.ok || res.status === 429) {
|
|
387
|
-
console.error(`NexusBloom MCP server connected to ${url}`);
|
|
388
|
-
connected = true;
|
|
389
|
-
break;
|
|
390
|
-
} else if (res.status === 404) {
|
|
391
|
-
// Try without /tools path
|
|
392
|
-
const res2 = await fetch(url, { ...FETCH_OPTS });
|
|
393
|
-
if (res2.ok) {
|
|
394
|
-
console.error(`NexusBloom MCP server connected to ${url}`);
|
|
395
|
-
connected = true;
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
} catch (_) {
|
|
400
|
-
// Continue to next candidate
|
|
401
|
-
}
|
|
402
|
-
}
|
|
71
|
+
return { ...app, transport, close: shutdown };
|
|
72
|
+
}
|
|
403
73
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`Checked endpoints:\n` +
|
|
409
|
-
apiUrlCandidates.map(u => ` - ${u.includes("/api") ? `${u}/tools` : `${u}/api/tools`}`).join("\n") + "\n" +
|
|
410
|
-
`To fix: ensure NEXUSBLOOM_API_URL is set correctly (default: https://nexusbloom.dev/api)\n` +
|
|
411
|
-
`Or deploy the platform API and verify the Vercel rewrite from your frontend project.`
|
|
412
|
-
);
|
|
413
|
-
}
|
|
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]}`;
|
|
414
78
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
} catch (err) {
|
|
419
|
-
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`);
|
|
420
82
|
process.exit(1);
|
|
421
|
-
}
|
|
83
|
+
});
|
|
422
84
|
}
|
|
423
|
-
|
|
424
|
-
main();
|
package/package.json
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexusbloom/mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server for NexusBloom —
|
|
3
|
+
"version": "2.0.0",
|
|
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
|
+
"lint": "node --check index.js && for f in src/*.js; do node --check \"$f\" || exit 1; done"
|
|
15
39
|
}
|
|
16
40
|
}
|