@thehammer/schema-mcp-server 1.0.13 → 1.0.15
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/dist/index.js +35 -31
- package/dist/version-check.d.ts +15 -1
- package/dist/version-check.js +57 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,22 +14,11 @@
|
|
|
14
14
|
* SCHEMA_API_TOKEN - Bearer token for SchemaMcpAuthMiddleware
|
|
15
15
|
* SCHEMA_DEFINITION_ID - ID of the schema being built
|
|
16
16
|
*/
|
|
17
|
-
import { createRequire } from "node:module";
|
|
18
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
19
|
import { z } from "zod";
|
|
21
20
|
import * as api from "./api-client.js";
|
|
22
|
-
import { assertVersionCurrent } from "./version-check.js";
|
|
23
|
-
const require = createRequire(import.meta.url);
|
|
24
|
-
const pkg = require("../package.json");
|
|
25
|
-
/**
|
|
26
|
-
* Version frozen into memory at module load. Compared against the on-disk
|
|
27
|
-
* package.json before every tool handler runs, so if the server's own
|
|
28
|
-
* installation is updated during runtime (npm publish, npm install,
|
|
29
|
-
* manual replacement) the next tool call halts the process instead of
|
|
30
|
-
* silently serving stale code.
|
|
31
|
-
*/
|
|
32
|
-
const LOADED_VERSION = pkg.version;
|
|
21
|
+
import { LOADED_VERSION, assertVersionCurrent } from "./version-check.js";
|
|
33
22
|
const server = new McpServer({
|
|
34
23
|
name: "schema-mcp-server",
|
|
35
24
|
version: LOADED_VERSION,
|
|
@@ -44,19 +33,27 @@ const server = new McpServer({
|
|
|
44
33
|
* then delegates to the original implementation. New tools added in the future
|
|
45
34
|
* are covered automatically — no per-tool bookkeeping required.
|
|
46
35
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
36
|
+
* Fail-loud invariants:
|
|
37
|
+
* - The handler is always the last argument across every current SDK overload.
|
|
38
|
+
* A runtime `typeof` guard throws immediately if a future SDK overload
|
|
39
|
+
* breaks this assumption — the wrapper never silently wraps the wrong arg.
|
|
40
|
+
* - `assertVersionCurrent()` reads package.json on every call via
|
|
41
|
+
* `fs.readFileSync` (never `require()`, which caches). Sync reads of a
|
|
42
|
+
* ~500-byte file are microsecond-scale — no throttling needed.
|
|
50
43
|
*/
|
|
51
|
-
const
|
|
44
|
+
const originalTool = server.tool.bind(server);
|
|
52
45
|
server.tool = (...args) => {
|
|
53
46
|
const handlerIdx = args.length - 1;
|
|
54
47
|
const handler = args[handlerIdx];
|
|
48
|
+
if (typeof handler !== "function") {
|
|
49
|
+
throw new Error("[schema-mcp-server] server.tool override: expected last argument to be a handler function. SDK overload change detected — rewire the override before continuing.");
|
|
50
|
+
}
|
|
51
|
+
const originalHandler = handler;
|
|
55
52
|
args[handlerIdx] = async (...handlerArgs) => {
|
|
56
|
-
assertVersionCurrent(
|
|
57
|
-
return
|
|
53
|
+
assertVersionCurrent();
|
|
54
|
+
return originalHandler(...handlerArgs);
|
|
58
55
|
};
|
|
59
|
-
return
|
|
56
|
+
return originalTool(...args);
|
|
60
57
|
};
|
|
61
58
|
/**
|
|
62
59
|
* Role-based tool registration.
|
|
@@ -113,7 +110,6 @@ const ROLE_TOOLS = {
|
|
|
113
110
|
"schema_update_model",
|
|
114
111
|
"schema_remove_model",
|
|
115
112
|
"schema_update_root",
|
|
116
|
-
"quality_gate_submit_review",
|
|
117
113
|
]),
|
|
118
114
|
"behavior-builder": new Set([
|
|
119
115
|
...COMMON_TOOLS,
|
|
@@ -121,7 +117,6 @@ const ROLE_TOOLS = {
|
|
|
121
117
|
"directive_create",
|
|
122
118
|
"directive_update",
|
|
123
119
|
"directive_delete",
|
|
124
|
-
"quality_gate_submit_review",
|
|
125
120
|
]),
|
|
126
121
|
"template-builder": new Set([
|
|
127
122
|
...COMMON_TOOLS,
|
|
@@ -130,7 +125,6 @@ const ROLE_TOOLS = {
|
|
|
130
125
|
"template_update",
|
|
131
126
|
"template_patch",
|
|
132
127
|
"template_set_sample_data",
|
|
133
|
-
"quality_gate_submit_review",
|
|
134
128
|
"style_list",
|
|
135
129
|
"annotation_create", // Question annotations for missing fields → orchestrator decides
|
|
136
130
|
]),
|
|
@@ -140,6 +134,7 @@ const ROLE_TOOLS = {
|
|
|
140
134
|
"style_list",
|
|
141
135
|
"context_workflow_inputs",
|
|
142
136
|
"context_workflow_input_pages",
|
|
137
|
+
"quality_gate_submit_review",
|
|
143
138
|
]),
|
|
144
139
|
};
|
|
145
140
|
/**
|
|
@@ -735,24 +730,33 @@ if (shouldRegister("quality_gate_submit_review"))
|
|
|
735
730
|
return jsonResult(result);
|
|
736
731
|
});
|
|
737
732
|
if (shouldRegister("quality_gate"))
|
|
738
|
-
server.tool("quality_gate", "
|
|
739
|
-
"
|
|
733
|
+
server.tool("quality_gate", "BLOCKING — dispatch a quality gate reviewer for a single category and wait for the " +
|
|
734
|
+
"result. This tool does NOT return until the reviewer finishes. Call this for each " +
|
|
735
|
+
"category you want reviewed (schema, directive, template) — call multiple categories " +
|
|
736
|
+
"in parallel so they run simultaneously. " +
|
|
737
|
+
"Response statuses: " +
|
|
740
738
|
"'passed' means all items in the category are green (safe to move on); " +
|
|
741
|
-
"'running' means a reviewer was dispatched (or one is already in flight) — do other " +
|
|
742
|
-
"productive work, then call this tool again later to check status; " +
|
|
743
739
|
"'failed' means items need fixes — read each item's `analysis`, apply fixes via " +
|
|
744
740
|
"mutation tools (which auto-invalidate the category), then call this tool again; " +
|
|
745
741
|
"'error' means the reviewer dispatch itself failed — surface the error to the user " +
|
|
746
|
-
"via an annotation and stop on that category (do NOT retry).
|
|
747
|
-
"You may call this for multiple categories in parallel. The backend is idempotent — " +
|
|
748
|
-
"calling while a reviewer is running returns 'running' without launching a duplicate. " +
|
|
749
|
-
"When you have no other productive work, continue polling — each call is cheap.", {
|
|
742
|
+
"via an annotation and stop on that category (do NOT retry).", {
|
|
750
743
|
category: z
|
|
751
744
|
.enum(["schema", "directive", "template"])
|
|
752
745
|
.describe("The quality gate category to check. Must be one of: schema, directive, template."),
|
|
753
746
|
message: messageParam,
|
|
754
747
|
}, async ({ category, message }) => {
|
|
755
|
-
|
|
748
|
+
let result = await api.callQualityGate(category, message);
|
|
749
|
+
// Poll until the reviewer finishes — the backend returns 'running'
|
|
750
|
+
// while a reviewer is in flight. We block here so the orchestrator
|
|
751
|
+
// agent gets a single tool result with the final verdict instead of
|
|
752
|
+
// burning dozens of tool calls in a polling loop.
|
|
753
|
+
while (result &&
|
|
754
|
+
typeof result === "object" &&
|
|
755
|
+
"status" in result &&
|
|
756
|
+
result.status === "running") {
|
|
757
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
758
|
+
result = await api.callQualityGate(category, message);
|
|
759
|
+
}
|
|
756
760
|
return jsonResult(result);
|
|
757
761
|
});
|
|
758
762
|
if (shouldRegister("complete"))
|
package/dist/version-check.d.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare const LOADED_VERSION: string;
|
|
2
|
+
/**
|
|
3
|
+
* Halt the process if the on-disk `package.json` version has drifted from
|
|
4
|
+
* `LOADED_VERSION`.
|
|
5
|
+
*
|
|
6
|
+
* Called from the `server.tool` override in `index.ts` before every tool
|
|
7
|
+
* handler runs. Uses `fs.readFileSync` + `JSON.parse` on every call —
|
|
8
|
+
* `require()` would cache the first read and defeat the check.
|
|
9
|
+
*
|
|
10
|
+
* Side effect: on mismatch (or if the disk read itself fails), writes a
|
|
11
|
+
* clear error line to stderr and calls `process.exit(1)`. There is no graceful
|
|
12
|
+
* shutdown window, no retry, no returned error object. The MCP stdio client
|
|
13
|
+
* sees the disconnect and surfaces it as a dispatch failure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertVersionCurrent(): void;
|
package/dist/version-check.js
CHANGED
|
@@ -1,20 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime version check
|
|
3
|
-
*
|
|
4
|
-
* fail loudly instead of serving old behavior silently.
|
|
2
|
+
* Runtime version check — single source of truth for the MCP server's version
|
|
3
|
+
* identity.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Owns BOTH:
|
|
6
|
+
* 1. `LOADED_VERSION`: the version captured into memory the first time this
|
|
7
|
+
* module is loaded (one fs read at module load).
|
|
8
|
+
* 2. `assertVersionCurrent()`: compares the on-disk package.json version to
|
|
9
|
+
* `LOADED_VERSION` and halts the process via `process.exit(1)` on
|
|
10
|
+
* mismatch so stale-code runs fail loudly instead of serving old behavior
|
|
11
|
+
* silently.
|
|
12
|
+
*
|
|
13
|
+
* Dist layout assumption: `../package.json` resolves from both
|
|
14
|
+
* `src/version-check.ts` AND `dist/version-check.js` to `mcp-server/package.json`
|
|
15
|
+
* because both files live one level below the package root. If tsconfig ever
|
|
16
|
+
* emits to a nested output dir (e.g. `dist/src/`), this assumption breaks and
|
|
17
|
+
* must be re-pinned here.
|
|
18
|
+
*
|
|
19
|
+
* Wired in once at the top of `index.ts` via a `server.tool` override — every
|
|
20
|
+
* tool handler runs the assertion before doing any work. See that override
|
|
21
|
+
* block for the single injection site.
|
|
9
22
|
*/
|
|
10
23
|
import fs from "node:fs";
|
|
11
24
|
import { fileURLToPath } from "node:url";
|
|
12
25
|
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Version frozen into memory at module load. Any later mismatch between this
|
|
28
|
+
* value and the on-disk `package.json` triggers `assertVersionCurrent()` to
|
|
29
|
+
* halt the process — there is no recovery path, no graceful shutdown, no retry.
|
|
30
|
+
*
|
|
31
|
+
* Read at module load via `fs.readFileSync` (not `require()`, which would
|
|
32
|
+
* cache the result and make the runtime check always pass). If `package.json`
|
|
33
|
+
* is missing or malformed at startup, the exception is intentionally
|
|
34
|
+
* uncaught — fail-loud is the correct behavior before the server even begins
|
|
35
|
+
* serving requests.
|
|
36
|
+
*/
|
|
37
|
+
const loadedPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
38
|
+
export const LOADED_VERSION = loadedPkg.version;
|
|
39
|
+
/**
|
|
40
|
+
* Halt the process if the on-disk `package.json` version has drifted from
|
|
41
|
+
* `LOADED_VERSION`.
|
|
42
|
+
*
|
|
43
|
+
* Called from the `server.tool` override in `index.ts` before every tool
|
|
44
|
+
* handler runs. Uses `fs.readFileSync` + `JSON.parse` on every call —
|
|
45
|
+
* `require()` would cache the first read and defeat the check.
|
|
46
|
+
*
|
|
47
|
+
* Side effect: on mismatch (or if the disk read itself fails), writes a
|
|
48
|
+
* clear error line to stderr and calls `process.exit(1)`. There is no graceful
|
|
49
|
+
* shutdown window, no retry, no returned error object. The MCP stdio client
|
|
50
|
+
* sees the disconnect and surfaces it as a dispatch failure.
|
|
51
|
+
*/
|
|
52
|
+
export function assertVersionCurrent() {
|
|
53
|
+
let diskVersion;
|
|
54
|
+
try {
|
|
55
|
+
const diskPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
56
|
+
diskVersion = diskPkg.version;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`[schema-mcp-server] VERSION CHECK FAILED: unable to read ${pkgPath}. Restart required. Cause:`, err);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (diskVersion !== LOADED_VERSION) {
|
|
63
|
+
console.error(`[schema-mcp-server] VERSION MISMATCH: loaded v${LOADED_VERSION} in memory, v${diskVersion} on disk. Restart required.`);
|
|
18
64
|
process.exit(1);
|
|
19
65
|
}
|
|
20
66
|
}
|
package/package.json
CHANGED