@nexusbloom/mcp-server 2.1.2 → 2.2.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 CHANGED
@@ -19,6 +19,9 @@
19
19
  * stdout is the MCP transport. All diagnostics go to stderr.
20
20
  */
21
21
 
22
+ import { realpathSync } from "node:fs";
23
+ import { pathToFileURL } from "node:url";
24
+
22
25
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
26
 
24
27
  import { ApiClient } from "./src/client.js";
@@ -85,8 +88,25 @@ export async function main(env = process.env, deps = {}) {
85
88
 
86
89
  // Only run when executed directly, so importing this module in a test does not
87
90
  // hijack stdio.
88
- const invokedDirectly =
89
- process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
91
+ //
92
+ // Both sides have to be normalised, and both mistakes here are silent:
93
+ // - `npx @nexusbloom/mcp-server` launches the `.bin/nexusbloom-mcp` symlink,
94
+ // so `process.argv[1]` is the symlink path while the ESM loader resolves
95
+ // `import.meta.url` to the real one. Comparing them raw reports "not
96
+ // invoked directly", the server never starts, and the process exits 0 with
97
+ // no output — a host just reports "no tools".
98
+ // - `import.meta.url` is percent-encoded, so a path containing a space or
99
+ // `#` never string-matches the raw `process.argv[1]`.
100
+ // `pathToFileURL(realpathSync(argv[1]))` puts both sides in the same form.
101
+ const invokedDirectly = (() => {
102
+ const entry = process.argv[1];
103
+ if (!entry) return false;
104
+ try {
105
+ return import.meta.url === pathToFileURL(realpathSync(entry)).href;
106
+ } catch {
107
+ return false;
108
+ }
109
+ })();
90
110
 
91
111
  if (invokedDirectly) {
92
112
  main().catch((err) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexusbloom/mcp-server",
3
- "version": "2.1.2",
4
- "description": "MCP server for NexusBloom \u2014 agents discover tools by intent, read exact schemas, and execute them. Built on @nexusbloom/core.",
3
+ "version": "2.2.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": {
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.8.0",
31
- "@nexusbloom/core": "^1.0.0"
31
+ "@nexusbloom/core": "^1.2.0"
32
32
  },
33
33
  "scripts": {
34
34
  "test": "node --test --import ./test/setup.mjs test/*.test.js",
package/src/manifests.js CHANGED
@@ -7,8 +7,42 @@
7
7
  * not. Everything downstream should be able to assume one shape.
8
8
  */
9
9
 
10
+ import * as coreContract from "@nexusbloom/core";
10
11
  import { NexusBloomError, ErrorCode } from "./errors.js";
11
12
 
13
+ /**
14
+ * The surface contract lives in `@nexusbloom/core` as of 1.2.0, but this package
15
+ * is consumed as `npx -y @nexusbloom/mcp-server`, which resolves its dependency
16
+ * fresh at install time. A static named import of a symbol the resolved core
17
+ * does not have is a *link* error, so the whole server would fail to start and
18
+ * an agent would get no tools at all — the worst failure this package can have,
19
+ * and one that depends on a publish order nobody controls.
20
+ *
21
+ * A namespace import cannot fail that way: a missing export is `undefined`
22
+ * rather than a crash. So the contract degrades to "nothing declared, nothing
23
+ * known" — the conservative reading — instead of taking the server down. Once
24
+ * core 1.2.0 is on the registry this resolves to the real implementation and the
25
+ * guard simply never fires.
26
+ */
27
+ const readContract = typeof coreContract.readContract === "function" ? coreContract.readContract : null;
28
+
29
+ const collectUnknownKeys =
30
+ typeof coreContract.collectUnknownKeys === "function" ? coreContract.collectUnknownKeys : () => [];
31
+
32
+ /** The shape `readContractFields` returns when no core contract is available. */
33
+ function emptyContractFields() {
34
+ return {
35
+ coreContract: null,
36
+ extensionContract: null,
37
+ side_effects: ["none"],
38
+ trust: "unverified",
39
+ surfaces: {},
40
+ unknownSideEffects: [],
41
+ contractIssues: [],
42
+ unknownManifestKeys: [],
43
+ };
44
+ }
45
+
12
46
  /** Empty but valid — a schema saying "takes nothing" is not the same as null. */
13
47
  export function emptyInputSchema() {
14
48
  return { type: "object", properties: {}, required: [] };
@@ -111,6 +145,49 @@ export function normaliseTool(raw) {
111
145
  // object — but an agent needs to know which one it is, because the first
112
146
  // means call `schema` before trying and the second means just call it.
113
147
  hasSchema: isSchemaPresent(raw.input_schema),
148
+
149
+ // ── Surface contract ────────────────────────────────────────────────────
150
+ // Everything below is additive and read-only. Nothing above changed shape,
151
+ // so a consumer that ignores these fields behaves exactly as before.
152
+ //
153
+ // The important property is that `surfaces` is *carried*, not filtered.
154
+ // This function used to be a closed whitelist: any key it did not name was
155
+ // dropped on the floor, which made per-surface customisation impossible to
156
+ // ship — an author's `mcp.agentHints` or `website.view` would vanish here
157
+ // with no error, and the only symptom would be a tool that looks correct in
158
+ // its own manifest and does nothing on the surface that asked for it.
159
+ // Preserving the block and reporting what we could not validate makes that
160
+ // failure mode impossible to hide.
161
+ ...readContractFields(raw),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Project the surface contract off a raw manifest row.
167
+ *
168
+ * Split out from `normaliseTool` so the projection can be tested on its own and
169
+ * reused by any surface that normalises a manifest the same way.
170
+ *
171
+ * @param {object} raw
172
+ * @returns {{coreContract: number|null, extensionContract: number|null,
173
+ * side_effects: string[], trust: string, surfaces: object,
174
+ * unknownSideEffects: string[], contractIssues: object[],
175
+ * unknownManifestKeys: string[]}}
176
+ */
177
+ export function readContractFields(raw) {
178
+ if (!readContract) return emptyContractFields();
179
+ const contract = readContract(raw);
180
+ return {
181
+ coreContract: contract.coreContract,
182
+ extensionContract: contract.extensionContract,
183
+ side_effects: contract.sideEffects.declared,
184
+ trust: contract.trust,
185
+ surfaces: contract.surfaces,
186
+ // Split out from `side_effects` so a consumer gating on capability does not
187
+ // have to re-derive which entries were recognised.
188
+ unknownSideEffects: contract.sideEffects.unknown,
189
+ contractIssues: contract.issues,
190
+ unknownManifestKeys: collectUnknownKeys(raw),
114
191
  };
115
192
  }
116
193