@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 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
- * The check reads package.json via fs.readFileSync + JSON.parse on every call.
48
- * `require()` would cache the first read and defeat the purpose. Sync fs reads
49
- * of a ~500-byte file are measured in microseconds, so no caching is needed.
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 __origTool = server.tool.bind(server);
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(LOADED_VERSION);
57
- return handler(...handlerArgs);
53
+ assertVersionCurrent();
54
+ return originalHandler(...handlerArgs);
58
55
  };
59
- return __origTool(...args);
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", "Dispatch or poll the quality gate for a single category (schema, directive, or template). " +
739
- "NON-BLOCKING the call returns immediately. Response statuses: " +
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
- const result = await api.callQualityGate(category, message);
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"))
@@ -1 +1,15 @@
1
- export declare function assertVersionCurrent(loadedVersion: string): void;
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;
@@ -1,20 +1,66 @@
1
1
  /**
2
- * Runtime version check: compares the MCP server's in-memory version to its
3
- * on-disk package.json and halts the process on mismatch so stale-code runs
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
- * Wired in once at the top of index.ts via a `server.tool` override — every
7
- * tool handler runs this check before doing any work. See the override block
8
- * in index.ts for the single call site.
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
- export function assertVersionCurrent(loadedVersion) {
14
- const diskVersion = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
15
- .version;
16
- if (diskVersion !== loadedVersion) {
17
- console.error(`[schema-mcp-server] VERSION MISMATCH: loaded v${loadedVersion} in memory, v${diskVersion} on disk. Restart required.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thehammer/schema-mcp-server",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "MCP server for Schema Builder - translates Claude Code tool calls into Laravel API requests",
5
5
  "license": "MIT",
6
6
  "repository": {