@nexusbloom/mcp-server 1.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/index.js +387 -0
- package/package.json +16 -0
package/index.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @nexusbloom/mcp-server
|
|
5
|
+
*
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx @nexusbloom/mcp-server
|
|
14
|
+
*
|
|
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)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
|
+
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
|
+
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
|
|
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
|
+
price_type: t.price_type || t.pricing || "free",
|
|
77
|
+
version: t.version || "1.0.0",
|
|
78
|
+
type: "builtin",
|
|
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
|
+
try {
|
|
94
|
+
const res = await fetch(`${API_URL}/tools/${encodeURIComponent(slug)}`, {
|
|
95
|
+
headers: apiHeaders(),
|
|
96
|
+
...FETCH_OPTS,
|
|
97
|
+
});
|
|
98
|
+
if (res.ok) {
|
|
99
|
+
const json = await res.json();
|
|
100
|
+
const tool = json.tool || json.data || json;
|
|
101
|
+
if (tool && tool.slug) {
|
|
102
|
+
return {
|
|
103
|
+
slug: tool.slug,
|
|
104
|
+
name: tool.name,
|
|
105
|
+
description: tool.short_description || tool.full_description || tool.description || "",
|
|
106
|
+
tags: (tool.tags || []).flat().filter(Boolean),
|
|
107
|
+
input_schema: tool.input_schema || {},
|
|
108
|
+
output_schema: tool.output_schema || {},
|
|
109
|
+
icon: tool.icon || "🔧",
|
|
110
|
+
price_type: tool.price_type || tool.pricing || "free",
|
|
111
|
+
version: tool.version || "1.0.0",
|
|
112
|
+
category: tool.category || "Platform Tools",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(`Failed to fetch schema for ${slug}:`, err.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fallback: try the manifest endpoint
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
|
|
123
|
+
headers: apiHeaders(),
|
|
124
|
+
...FETCH_OPTS,
|
|
125
|
+
});
|
|
126
|
+
if (res.ok) {
|
|
127
|
+
const json = await res.json();
|
|
128
|
+
const manifest = json.manifest || json.data?.manifest || json;
|
|
129
|
+
if (manifest && manifest.slug) {
|
|
130
|
+
return {
|
|
131
|
+
slug: manifest.slug,
|
|
132
|
+
name: manifest.name,
|
|
133
|
+
description: manifest.short_description || "",
|
|
134
|
+
tags: manifest.tags || [],
|
|
135
|
+
input_schema: manifest.input_schema || {},
|
|
136
|
+
output_schema: manifest.output_schema || {},
|
|
137
|
+
icon: manifest.icon || "🔧",
|
|
138
|
+
price_type: manifest.pricing || "free",
|
|
139
|
+
version: manifest.version || "1.0.0",
|
|
140
|
+
category: manifest.category || "Platform Tools",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error(`Failed to fetch manifest for ${slug}:`, err.message);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Execute a tool
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
async function executeTool(slug, params) {
|
|
156
|
+
const res = await fetch(`${API_URL}/run/${encodeURIComponent(slug)}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: apiHeaders(),
|
|
159
|
+
body: JSON.stringify({ input: params }),
|
|
160
|
+
...FETCH_OPTS,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
165
|
+
throw new Error(err.error || `API returned ${res.status}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const json = await res.json();
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
data: json.data || json,
|
|
172
|
+
tool: slug,
|
|
173
|
+
execution: "live",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// MCP Server
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
const server = new Server(
|
|
182
|
+
{
|
|
183
|
+
name: "nexusbloom-mcp",
|
|
184
|
+
version: "1.0.0",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
capabilities: {
|
|
188
|
+
tools: {},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// ---- ListTools ----
|
|
194
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
195
|
+
const tools = await fetchTools();
|
|
196
|
+
|
|
197
|
+
const toolList = tools.map((t) => ({
|
|
198
|
+
name: t.slug,
|
|
199
|
+
description: `${t.name}: ${t.short_description || "No description"}${
|
|
200
|
+
t.category ? ` [${t.category}]` : ""
|
|
201
|
+
}`,
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
command: {
|
|
206
|
+
type: "string",
|
|
207
|
+
enum: ["list", "schema", "run"],
|
|
208
|
+
description:
|
|
209
|
+
"What to do: 'list' returns all available tools, 'schema' returns a tool's full input/output schema, 'run' executes a tool",
|
|
210
|
+
},
|
|
211
|
+
slug: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description:
|
|
214
|
+
"Tool slug (required for 'schema' and 'run' commands). Use 'list' first to see available slugs.",
|
|
215
|
+
},
|
|
216
|
+
params: {
|
|
217
|
+
type: "object",
|
|
218
|
+
description:
|
|
219
|
+
"Input parameters matching the tool's input_schema (required for 'run' command). Use 'schema' first to see the expected format.",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
required: ["command"],
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
return { tools: toolList };
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---- CallTool ----
|
|
230
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
231
|
+
const { name: toolName, arguments: args } = request.params;
|
|
232
|
+
|
|
233
|
+
if (toolName !== "nexusbloom") {
|
|
234
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const command = args?.command;
|
|
238
|
+
|
|
239
|
+
if (!command || !["list", "schema", "run"].includes(command)) {
|
|
240
|
+
return {
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "text",
|
|
244
|
+
text: "Invalid command. Use one of: 'list', 'schema', 'run'.",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
isError: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- COMMAND: list ----
|
|
252
|
+
if (command === "list") {
|
|
253
|
+
const tools = await fetchTools();
|
|
254
|
+
|
|
255
|
+
const summary = tools
|
|
256
|
+
.map((t) => `• ${t.icon} **${t.name}** (\`${t.slug}\`) — ${t.short_description || "No description"} [${t.category}]`)
|
|
257
|
+
.join("\n");
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
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\`.`,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: "resource",
|
|
267
|
+
resource: {
|
|
268
|
+
text: JSON.stringify(tools, null, 2),
|
|
269
|
+
uri: "data:application/json,",
|
|
270
|
+
mimeType: "application/json",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- COMMAND: schema ----
|
|
278
|
+
if (command === "schema") {
|
|
279
|
+
const slug = args?.slug;
|
|
280
|
+
if (!slug) {
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
|
|
283
|
+
isError: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const schema = await fetchToolSchema(slug);
|
|
288
|
+
if (!schema) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: `Tool "${slug}" not found. Use 'list' to see available tools.` }],
|
|
291
|
+
isError: true,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const inputProps = schema.input_schema?.properties || {};
|
|
296
|
+
const requiredFields = schema.input_schema?.required || [];
|
|
297
|
+
const paramDocs = Object.entries(inputProps)
|
|
298
|
+
.map(([key, prop]) => {
|
|
299
|
+
const req = requiredFields.includes(key) ? " (required)" : "";
|
|
300
|
+
const def = prop.default !== undefined ? ` (default: ${JSON.stringify(prop.default)})` : "";
|
|
301
|
+
const enum_ = prop.enum ? ` [${prop.enum.join(", ")}]` : "";
|
|
302
|
+
return ` • \`${key}\`: ${prop.type || "any"}${req}${def}${enum_} — ${prop.description || ""}`;
|
|
303
|
+
})
|
|
304
|
+
.join("\n");
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
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.`,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
type: "resource",
|
|
314
|
+
resource: {
|
|
315
|
+
text: JSON.stringify(schema, null, 2),
|
|
316
|
+
uri: "data:application/json,",
|
|
317
|
+
mimeType: "application/json",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---- COMMAND: run ----
|
|
325
|
+
if (command === "run") {
|
|
326
|
+
const slug = args?.slug;
|
|
327
|
+
const params = args?.params || {};
|
|
328
|
+
|
|
329
|
+
if (!slug) {
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: "Missing required parameter: 'slug'" }],
|
|
332
|
+
isError: true,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const result = await executeTool(slug, params);
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: `# Tool Result: ${slug}\n\n**Execution:** ${result.execution}\n\`\`\`json\n${JSON.stringify(result.data, null, 2)}\n\`\`\``,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
type: "resource",
|
|
346
|
+
resource: {
|
|
347
|
+
text: JSON.stringify(result, null, 2),
|
|
348
|
+
uri: "data:application/json,",
|
|
349
|
+
mimeType: "application/json",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
};
|
|
354
|
+
} catch (err) {
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: `Error executing "${slug}": ${err.message}` }],
|
|
357
|
+
isError: true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ---- Start ----
|
|
364
|
+
async function main() {
|
|
365
|
+
try {
|
|
366
|
+
// Verify API connectivity
|
|
367
|
+
try {
|
|
368
|
+
const res = await fetch(`${API_URL}/tools`, { ...FETCH_OPTS });
|
|
369
|
+
if (res.ok) {
|
|
370
|
+
console.error(`NexusBloom MCP server connected to ${API_URL}`);
|
|
371
|
+
} else {
|
|
372
|
+
console.error(`WARNING: API at ${API_URL} returned ${res.status}`);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.error(`WARNING: Could not reach API at ${API_URL}: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const transport = new StdioServerTransport();
|
|
379
|
+
await server.connect(transport);
|
|
380
|
+
console.error("NexusBloom MCP server running on stdio");
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error("Fatal error:", err);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexusbloom/mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nexusbloom-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.8.0"
|
|
15
|
+
}
|
|
16
|
+
}
|