@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.
Files changed (2) hide show
  1. package/index.js +387 -0
  2. 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
+ }