@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 +22 -2
- package/package.json +3 -3
- package/src/manifests.js +77 -0
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
|
-
|
|
89
|
-
|
|
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.
|
|
4
|
-
"description": "MCP server for NexusBloom
|
|
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.
|
|
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
|
|