@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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Input validation — delegated to @nexusbloom/core so an agent is told the
3
+ * same thing the CLI tells a human.
4
+ *
5
+ * Validating before the request is not just politeness: it saves a round trip,
6
+ * and it turns a 400 into an error the agent can act on immediately because the
7
+ * message names the exact field and what was expected.
8
+ *
9
+ * The API remains the authority. This layer only rejects input that core has
10
+ * proven invalid, so a false positive here would block a call that would have
11
+ * succeeded — the check is therefore advisory-silent on anything it cannot
12
+ * prove wrong.
13
+ */
14
+
15
+ import { validateInput } from "@nexusbloom/core/types";
16
+ import { ErrorCode, NexusBloomError } from "./errors.js";
17
+
18
+ /**
19
+ * Validate params against a tool's input schema.
20
+ *
21
+ * @param {object} params
22
+ * @param {object} inputSchema
23
+ * @param {string} slug
24
+ * @throws {NexusBloomError} VALIDATION_FAILED, carrying per-field detail.
25
+ */
26
+ export function assertValidInput(params, inputSchema, slug) {
27
+ const schema = inputSchema && typeof inputSchema === "object" ? inputSchema : null;
28
+
29
+ // No schema means the API is the only validator available; defer rather than
30
+ // reject a call that might legitimately succeed.
31
+ if (!schema || !schema.properties || Object.keys(schema.properties).length === 0) {
32
+ if (Array.isArray(schema?.required) && schema.required.length > 0) {
33
+ const missing = schema.required.filter((k) => params?.[k] === undefined || params?.[k] === null);
34
+ if (missing.length) {
35
+ throw new NexusBloomError(
36
+ `Missing required ${missing.length === 1 ? "parameter" : "parameters"} for "${slug}": ${missing.join(", ")}`,
37
+ ErrorCode.VALIDATION_FAILED,
38
+ { details: { missing } },
39
+ );
40
+ }
41
+ }
42
+ return params;
43
+ }
44
+
45
+ // core returns `[]` when valid. A null/undefined return would mean it could
46
+ // not decide, which must not be read as a pass or a failure.
47
+ let errors;
48
+ try {
49
+ errors = validateInput(params, schema);
50
+ } catch {
51
+ return params; // validator itself failed — defer to the API
52
+ }
53
+ if (!Array.isArray(errors)) return params;
54
+ if (errors.length === 0) return params;
55
+
56
+ // core returns `{ field, error }` objects, not strings. Joining them raw
57
+ // produces "[object Object]", which tells an agent nothing about what to fix.
58
+ const described = errors.map((e) =>
59
+ e && typeof e === "object"
60
+ ? `${e.field ?? "input"}: ${e.error ?? "invalid"}`
61
+ : String(e),
62
+ );
63
+
64
+ const missing = (Array.isArray(schema.required) ? schema.required : []).filter(
65
+ (k) => params?.[k] === undefined || params?.[k] === null,
66
+ );
67
+
68
+ throw new NexusBloomError(
69
+ `Invalid input for "${slug}": ${described.join("; ")}`,
70
+ ErrorCode.VALIDATION_FAILED,
71
+ { details: { errors: described, missing } },
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Coerce params that arrived as a JSON string.
77
+ *
78
+ * Some hosts serialise object arguments to a string. Rejecting that would fail
79
+ * a call that is well-formed underneath, so a parseable string is unwrapped and
80
+ * a genuinely unparseable one is reported rather than passed through to the API
81
+ * as a bare string body.
82
+ */
83
+ export function coerceParams(params, slug) {
84
+ if (typeof params !== "string") return params ?? {};
85
+ const trimmed = params.trim();
86
+ if (!trimmed) return {};
87
+
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(trimmed);
91
+ } catch {
92
+ throw new NexusBloomError(
93
+ `"params" for "${slug}" was a string but is not valid JSON. Pass an object, e.g. {"key": "value"}.`,
94
+ ErrorCode.INVALID_ARGS,
95
+ );
96
+ }
97
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
98
+ throw new NexusBloomError(
99
+ `"params" for "${slug}" must be a JSON object, got ${Array.isArray(parsed) ? "an array" : typeof parsed}.`,
100
+ ErrorCode.INVALID_ARGS,
101
+ );
102
+ }
103
+ return parsed;
104
+ }
@@ -1,104 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * MCP Server Test — uses the MCP SDK Client directly.
4
- * Tests both direct slug execution and meta-tool paths.
5
- *
6
- * Usage: node test-mcp-client.mjs
7
- */
8
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
-
11
- async function main() {
12
- console.log("═══ MCP Server Test ═══\n");
13
-
14
- const transport = new StdioClientTransport({
15
- command: "npx",
16
- args: ["@nexusbloom/mcp-server"],
17
- });
18
-
19
- const client = new Client(
20
- { name: "mcp-test-client", version: "1.0.0" },
21
- { capabilities: {} }
22
- );
23
-
24
- await client.connect(transport);
25
- console.log("✅ Connected to MCP server\n");
26
-
27
- // ── Test 1: ListTools ─────────────────────────────────────────
28
- const { tools } = await client.listTools();
29
- console.log(`✅ Tools available: ${tools.length}`);
30
- const slugs = tools.filter((t) => t.name !== "nexusbloom").map((t) => t.name);
31
- const metaTool = tools.find((t) => t.name === "nexusbloom");
32
- if (metaTool) console.log(`✅ Meta-tool "nexusbloom" registered`);
33
- console.log(` Example tools: ${slugs.slice(0, 5).join(", ")}...`);
34
- console.log();
35
-
36
- // ── Test 2: Direct slug execution ─────────────────────────────
37
- // AI agents call tools by their slug name directly
38
- console.log("── Test: Direct slug execution ──");
39
- const cronResult = await client.callTool({
40
- name: "cron-expression-builder",
41
- arguments: { expression: "0 0 * * *" },
42
- });
43
- const cronText = cronResult.content.find((c) => c.type === "text")?.text || "";
44
- if (cronText.includes(`"valid": true`)) {
45
- console.log("✅ cron-expression-builder executed directly by slug");
46
- console.log(` → ${cronText.split("\n")[1] || "valid=true"}`);
47
- } else {
48
- console.log(`⚠ Unexpected: ${cronText.substring(0, 100)}`);
49
- }
50
-
51
- const dataResult = await client.callTool({
52
- name: "data-generator",
53
- arguments: { count: 2 },
54
- });
55
- const dataText = dataResult.content.find((c) => c.type === "text")?.text || "";
56
- if (dataText.includes("data-generator")) {
57
- console.log("✅ data-generator executed directly by slug");
58
- }
59
- console.log();
60
-
61
- // ── Test 3: Meta-tool "list" ──────────────────────────────────
62
- console.log("── Test: Meta-tool list ──");
63
- const listResult = await client.callTool({
64
- name: "nexusbloom",
65
- arguments: { command: "list" },
66
- });
67
- const listText = listResult.content.find((c) => c.type === "text")?.text || "";
68
- if (listText.includes("NexusBloom Tools")) {
69
- console.log("✅ Meta-tool list works");
70
- }
71
- console.log();
72
-
73
- // ── Test 4: Meta-tool "schema" ────────────────────────────────
74
- console.log("── Test: Meta-tool schema ──");
75
- const schemaResult = await client.callTool({
76
- name: "nexusbloom",
77
- arguments: { command: "schema", slug: "cron-expression-builder" },
78
- });
79
- const schemaText = schemaResult.content.find((c) => c.type === "text")?.text || "";
80
- if (schemaText.includes("Cron Expression Builder")) {
81
- console.log("✅ Meta-tool schema works");
82
- }
83
- console.log();
84
-
85
- // ── Test 5: Meta-tool "run" ───────────────────────────────────
86
- console.log("── Test: Meta-tool run ──");
87
- const runResult = await client.callTool({
88
- name: "nexusbloom",
89
- arguments: { command: "run", slug: "data-generator", params: { count: 3 } },
90
- });
91
- const runText = runResult.content.find((c) => c.type === "text")?.text || "";
92
- if (runText.includes("data-generator")) {
93
- console.log("✅ Meta-tool run works");
94
- }
95
- console.log();
96
-
97
- await client.close();
98
- console.log("═══ All MCP tests passed! ═══");
99
- }
100
-
101
- main().catch((err) => {
102
- console.error("❌ MCP test failed:", err.message);
103
- process.exit(1);
104
- });