@questpie/mcp 3.28.9 → 3.28.11

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.
@@ -128,7 +128,7 @@ const DEFAULT_MCP_CONFIG = {
128
128
  stdio: { trustedMaintenance: false },
129
129
  execution: {}
130
130
  };
131
- function isPlainObject(value) {
131
+ function isPlainObject$1(value) {
132
132
  return !!value && typeof value === "object" && !Array.isArray(value);
133
133
  }
134
134
  const UNSAFE_CONFIG_KEYS = new Set([
@@ -137,7 +137,7 @@ const UNSAFE_CONFIG_KEYS = new Set([
137
137
  "constructor"
138
138
  ]);
139
139
  function ownDataEntries(value) {
140
- if (!isPlainObject(value)) return [];
140
+ if (!isPlainObject$1(value)) return [];
141
141
  try {
142
142
  return Object.entries(Object.getOwnPropertyDescriptors(value)).filter(([key, descriptor]) => !UNSAFE_CONFIG_KEYS.has(key) && "value" in descriptor).map(([key, descriptor]) => [key, descriptor.value]);
143
143
  } catch {
@@ -146,14 +146,14 @@ function ownDataEntries(value) {
146
146
  }
147
147
  function safeCloneConfigValue(value) {
148
148
  if (Array.isArray(value)) return value.map((entry) => safeCloneConfigValue(entry));
149
- if (!isPlainObject(value)) return value;
149
+ if (!isPlainObject$1(value)) return value;
150
150
  const clone = Object.create(null);
151
151
  for (const [key, entry] of ownDataEntries(value)) clone[key] = safeCloneConfigValue(entry);
152
152
  return clone;
153
153
  }
154
154
  function mergeDeep(base, override) {
155
155
  const out = safeCloneConfigValue(base);
156
- for (const [key, value] of ownDataEntries(override)) if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeDeep(out[key], value);
156
+ for (const [key, value] of ownDataEntries(override)) if (isPlainObject$1(value) && isPlainObject$1(out[key])) out[key] = mergeDeep(out[key], value);
157
157
  else if (value !== void 0) out[key] = safeCloneConfigValue(value);
158
158
  return out;
159
159
  }
@@ -615,13 +615,37 @@ const PUBLIC_ERROR_CODES = new Set([
615
615
  var McpPublicError = class extends Error {
616
616
  code;
617
617
  correlationId;
618
- constructor(code, correlationId = randomUUID()) {
619
- super(`${publicMessage(code)} (${code}; correlationId=${correlationId})`);
618
+ /** Caller-safe text appended to the public message; see {@link mcpInvalidInputError}. */
619
+ detail;
620
+ constructor(code, correlationId = randomUUID(), detail) {
621
+ super(`${publicMessage(code, detail)} (${code}; correlationId=${correlationId})`);
620
622
  this.name = "McpPublicError";
621
623
  this.code = code;
622
624
  this.correlationId = correlationId;
625
+ this.detail = detail;
623
626
  }
624
627
  };
628
+ const MAX_INPUT_ISSUES = 8;
629
+ const MAX_ISSUE_PATH_SEGMENT_CHARS = 128;
630
+ const MAX_ISSUE_MESSAGE_CHARS = 200;
631
+ const MAX_INPUT_DETAIL_CHARS = 4096;
632
+ /**
633
+ * The caller's own arguments failed the tool's input schema. Zod's own
634
+ * messages name only schema facts and the caller's keys, so the paths and
635
+ * messages go back to the caller — without them it can only guess which field
636
+ * to fix. Messages an app writes in `refine`/`superRefine`/`addIssue` are
637
+ * returned verbatim, exactly as the MCP SDK already returns them over HTTP;
638
+ * they must not carry server data. Only the input parse may use this: a
639
+ * ZodError thrown later, inside a handler, stays the opaque `invalid_input`.
640
+ */
641
+ function mcpInvalidInputError(error, correlationId) {
642
+ const issues = error.issues.slice(0, MAX_INPUT_ISSUES).map((issue) => {
643
+ return `${issue.path.map((segment) => String(segment).slice(0, MAX_ISSUE_PATH_SEGMENT_CHARS)).join(".") || "(root)"}: ${issue.message.slice(0, MAX_ISSUE_MESSAGE_CHARS)}`;
644
+ });
645
+ const omitted = error.issues.length - issues.length;
646
+ if (omitted > 0) issues.push(`(${omitted} more)`);
647
+ return new McpPublicError("invalid_input", correlationId, `invalid input — ${issues.join("; ")}`.slice(0, MAX_INPUT_DETAIL_CHARS));
648
+ }
625
649
  var McpAccessDeniedError = class extends Error {
626
650
  constructor() {
627
651
  super("MCP access denied");
@@ -762,8 +786,9 @@ function snapshotBoundedMcpValue(value, limits, code, correlationId = randomUUID
762
786
  if (!snapshot.ok) throw new McpPublicError(code, correlationId);
763
787
  return snapshot.value;
764
788
  }
765
- function publicMessage(code) {
766
- return code === "access_denied" ? "MCP access denied" : "MCP operation failed";
789
+ function publicMessage(code, detail) {
790
+ if (code === "access_denied") return "MCP access denied";
791
+ return detail ? `MCP operation failed: ${detail}` : "MCP operation failed";
767
792
  }
768
793
  function classifyError(error, correlationId) {
769
794
  if (!!error && (typeof error === "object" || typeof error === "function") && types.isProxy(error)) return new McpPublicError("internal", correlationId);
@@ -825,7 +850,7 @@ function toMcpToolError(error) {
825
850
  isError: true,
826
851
  content: [{
827
852
  type: "text",
828
- text: publicMessage(resolved.code)
853
+ text: publicMessage(resolved.code, resolved.detail)
829
854
  }],
830
855
  _meta: { [PUBLIC_ERROR_META_KEY]: {
831
856
  code: resolved.code,
@@ -1048,11 +1073,123 @@ function toJsonSchema(schema) {
1048
1073
  const compatible = jsonSchemaCompatibleSchema(schema);
1049
1074
  if (!compatible) return void 0;
1050
1075
  try {
1051
- return z.toJSONSchema(compatible, { reused: "ref" });
1076
+ return applyMcpSchemaDiet(z.toJSONSchema(compatible, { reused: "ref" }));
1052
1077
  } catch {
1053
1078
  return;
1054
1079
  }
1055
1080
  }
1081
+ function isPlainObject(value) {
1082
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1083
+ }
1084
+ const SCHEMA_MAP_KEYS = [
1085
+ "properties",
1086
+ "patternProperties",
1087
+ "dependentSchemas",
1088
+ "$defs",
1089
+ "definitions"
1090
+ ];
1091
+ const NEUTRAL_SCHEMA_LIST_KEYS = [
1092
+ "anyOf",
1093
+ "allOf",
1094
+ "prefixItems"
1095
+ ];
1096
+ const NEUTRAL_SCHEMA_KEYS = [
1097
+ "items",
1098
+ "then",
1099
+ "else",
1100
+ "propertyNames",
1101
+ "contains"
1102
+ ];
1103
+ const NEUTRAL_MAYBE_BOOLEAN_SCHEMA_KEYS = [
1104
+ "additionalProperties",
1105
+ "unevaluatedProperties",
1106
+ "unevaluatedItems",
1107
+ "additionalItems"
1108
+ ];
1109
+ /**
1110
+ * Rewrites a draft-07-style tuple (`items: [...]` + `additionalItems`) into
1111
+ * its 2020-12 equivalent (`prefixItems: [...]` + trailing `items`) in place.
1112
+ *
1113
+ * The MCP SDK's own `tools/list` projection (`zod-json-schema-compat.js`,
1114
+ * called with no `target`) emits JSON Schema **draft-07**, not 2020-12 — the
1115
+ * only place that difference is load-bearing for this codebase's schemas is
1116
+ * tuple typing, where draft-07's array-form `items` means something entirely
1117
+ * different in 2020-12 (a single "all items must match" schema). Our own
1118
+ * `toJsonSchema()` below (full zod's `z.toJSONSchema`) already emits native
1119
+ * 2020-12 `prefixItems`, so this is a no-op there. Converting first — for
1120
+ * every tuple anywhere in the tree, not just the root — makes the whole
1121
+ * document genuinely dialect-correct 2020-12 before `applyMcpSchemaDiet`
1122
+ * drops the top-level `$schema` pointer.
1123
+ */
1124
+ function normalizeTupleDialect(node) {
1125
+ if (!Array.isArray(node.items) || node.prefixItems !== void 0) return;
1126
+ const tuple = node.items;
1127
+ const additionalItems = node.additionalItems;
1128
+ delete node.items;
1129
+ delete node.additionalItems;
1130
+ node.prefixItems = tuple;
1131
+ if (additionalItems === false) node.items = false;
1132
+ else if (isPlainObject(additionalItems)) node.items = additionalItems;
1133
+ }
1134
+ /**
1135
+ * Strips wire noise the zod v4 / MCP SDK JSON Schema codecs emit on every
1136
+ * tool schema, without touching example/default/const/enum payloads (those
1137
+ * carry caller data, not schema structure, and must never be walked).
1138
+ *
1139
+ * - Removes the document-level `$schema` dialect pointer (informational only;
1140
+ * `normalizeTupleDialect` above makes the document genuinely 2020-12-shaped
1141
+ * first, so an absent `$schema` is universally, correctly, read as 2020-12).
1142
+ * - Drops `additionalProperties: false`, *except inside a `oneOf`/`not`/`if`
1143
+ * subtree*. Ordinarily this is safe to drop everywhere: the MCP SDK
1144
+ * validates tool arguments against the *original* zod schema at call time
1145
+ * (`McpServer#validateToolInput` in `mcp.js`), never against this projected
1146
+ * JSON Schema, so removing the annotation does not change what a call gets
1147
+ * rejected for. But inside `oneOf`, `additionalProperties: false` can be
1148
+ * the only thing making two branches mutually exclusive (reproduced:
1149
+ * `z.xor` of two `.strict()` objects with disjoint keys — stripping it from
1150
+ * both branches makes a value that should match exactly one branch match
1151
+ * both, which every `oneOf`-aware validator then rejects as ambiguous, even
1152
+ * though the MCP server itself would still accept it). Inside `not`, it can
1153
+ * be the very thing being negated — stripping it *widens* what `not`
1154
+ * excludes, which can turn a previously-schema-valid value into
1155
+ * schema-invalid for any client-side validator. `if` shares the same
1156
+ * "widening changes routing" hazard. `anyOf`/`allOf` don't have this
1157
+ * problem (a widened disjunct can never invalidate an already-valid input),
1158
+ * so only `oneOf`/`not`/`if` propagate the protection flag.
1159
+ * - Drops the `pattern` on a `format: "uuid"` node, keeping `format`. The
1160
+ * pattern is the zod-generated uuid regex that always accompanies
1161
+ * `format: "uuid"` in this codebase's schemas (there is no path that sets
1162
+ * `format: "uuid"` other than zod's own `.uuid()`), and — same reasoning as
1163
+ * `additionalProperties` — the MCP SDK never validates against this
1164
+ * projected schema, so the regex is redundant here. `pattern` on any other
1165
+ * node — a node without `format: "uuid"` — is a user-authored constraint
1166
+ * and is left untouched.
1167
+ */
1168
+ function walkSchemaNode(node, protectAdditionalProperties = false) {
1169
+ if (!isPlainObject(node)) return;
1170
+ normalizeTupleDialect(node);
1171
+ if (node.format === "uuid" && typeof node.pattern === "string") delete node.pattern;
1172
+ if (!protectAdditionalProperties && node.additionalProperties === false) delete node.additionalProperties;
1173
+ for (const key of SCHEMA_MAP_KEYS) {
1174
+ const map = node[key];
1175
+ if (isPlainObject(map)) for (const value of Object.values(map)) walkSchemaNode(value, protectAdditionalProperties);
1176
+ }
1177
+ for (const key of NEUTRAL_SCHEMA_LIST_KEYS) {
1178
+ const list = node[key];
1179
+ if (Array.isArray(list)) for (const item of list) walkSchemaNode(item, protectAdditionalProperties);
1180
+ }
1181
+ for (const key of NEUTRAL_SCHEMA_KEYS) walkSchemaNode(node[key], protectAdditionalProperties);
1182
+ for (const key of NEUTRAL_MAYBE_BOOLEAN_SCHEMA_KEYS) walkSchemaNode(node[key], protectAdditionalProperties);
1183
+ if (Array.isArray(node.oneOf)) for (const item of node.oneOf) walkSchemaNode(item, true);
1184
+ walkSchemaNode(node.not, true);
1185
+ walkSchemaNode(node.if, true);
1186
+ }
1187
+ function applyMcpSchemaDiet(schema) {
1188
+ if (!isPlainObject(schema)) return schema;
1189
+ delete schema.$schema;
1190
+ walkSchemaNode(schema);
1191
+ return schema;
1192
+ }
1056
1193
  function toToolInputJsonSchema(schema) {
1057
1194
  const parsed = ToolSchema.shape.inputSchema.safeParse(toJsonSchema(schema));
1058
1195
  return parsed.success ? parsed.data : {
@@ -1989,7 +2126,9 @@ async function executeCustomToolCall(scope, toolName, tool, input, extra) {
1989
2126
  },
1990
2127
  concurrencyKey: () => workloadAuthorization?.concurrencyKey,
1991
2128
  invoke: async ({ ctx, signal, requestId, correlationId }) => {
1992
- const parsedInput = tool.config.inputSchema ? await tool.config.inputSchema.parseAsync(input) : await z.object({}).strict().parseAsync(input);
2129
+ const parsed = await (tool.config.inputSchema ?? z.object({}).strict()).safeParseAsync(input);
2130
+ if (!parsed.success) throw mcpInvalidInputError(parsed.error, correlationId);
2131
+ const parsedInput = parsed.data;
1993
2132
  const invoke = () => tool.handler({
1994
2133
  input: parsedInput,
1995
2134
  ctx,
@@ -2358,6 +2497,46 @@ async function registerRouteTools(server, scope, catalog) {
2358
2497
 
2359
2498
  //#endregion
2360
2499
  //#region src/server/create-server.ts
2500
+ function isProductionEnvironment() {
2501
+ return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
2502
+ }
2503
+ /**
2504
+ * Reports a schema-diet install failure loudly instead of either crashing
2505
+ * every session (a renamed/removed SDK internal would otherwise throw deep
2506
+ * inside request handling on the very first `tools/list` call) or failing
2507
+ * silently (shipping an undieted catalogue with nobody told why). Always
2508
+ * logs; only throws outside production, so a CI/dev run surfaces the break
2509
+ * immediately while a live server degrades to the SDK's own undieted
2510
+ * `tools/list` response.
2511
+ */
2512
+ function reportSchemaDietInstallFailure(message) {
2513
+ const prefixed = `@questpie/mcp: ${message}`;
2514
+ console.error(prefixed);
2515
+ if (!isProductionEnvironment()) throw new Error(prefixed);
2516
+ }
2517
+ function installSchemaDietListToolsHandler(server) {
2518
+ const handlers = Reflect.get(server.server, "_requestHandlers");
2519
+ if (!(handlers instanceof Map)) {
2520
+ reportSchemaDietInstallFailure("could not install the tools/list schema diet — McpServer#server's internal request-handler map is missing or not a Map (the MCP SDK's internals likely changed shape). Tools will be listed without the diet applied.");
2521
+ return;
2522
+ }
2523
+ const defaultHandler = handlers.get("tools/list");
2524
+ if (!defaultHandler) {
2525
+ reportSchemaDietInstallFailure("could not install the tools/list schema diet — no default tools/list handler was registered even though at least one tool exists. Tools will be listed without the diet applied.");
2526
+ return;
2527
+ }
2528
+ server.server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
2529
+ const result = await defaultHandler(request, extra);
2530
+ return {
2531
+ ...result,
2532
+ tools: result.tools.map((tool) => ({
2533
+ ...tool,
2534
+ inputSchema: applyMcpSchemaDiet(tool.inputSchema),
2535
+ ...tool.outputSchema ? { outputSchema: applyMcpSchemaDiet(tool.outputSchema) } : {}
2536
+ }))
2537
+ };
2538
+ });
2539
+ }
2361
2540
  async function createServer(app, options, workload) {
2362
2541
  const release = workload ? createWorkloadMcpRelease(app, options.config) : options.config ? createIsolatedMcpRelease(app, options.config) : getAppMcpRelease(app);
2363
2542
  const { config, catalog } = release;
@@ -2382,6 +2561,7 @@ async function createServer(app, options, workload) {
2382
2561
  await registerCustomTools(server, scope, catalog);
2383
2562
  if (workload) server.server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => listWorkloadTools(workload, release.execution, extra));
2384
2563
  else if ([...catalog.collections.values()].every((entry) => entry.operations.length === 0) && [...catalog.globals.values()].every((entry) => entry.operations.length === 0) && catalog.routes.size === 0 && catalog.customTools.size === 0) server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
2564
+ else installSchemaDietListToolsHandler(server);
2385
2565
  return server;
2386
2566
  }
2387
2567
  function resolveStdioAccessMode(options, trustedMaintenance) {
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { S as scopesFromContext, _ as resolveMcpCatalog, a as customToolWorkloadIdentity, b as requiredScopesForOperation, c as createWorkloadMcpBoundary, f as McpAccessDeniedError, g as toMcpToolError, h as mcpPublicErrorCode, i as customToolDescriptor, l as createRuntimeScope, m as assertBoundedMcpValue, n as createWorkloadMcpServer, o as executeCustomToolCall, p as McpPublicError, r as customToolAllows, s as authorizeWorkload, t as createMcpServer, u as createWorkloadMcpRelease, v as defaultOperationScope, w as mcpTool, x as scopeGateAllows, y as normalizeRequiredScopes } from "./create-server-Ty80vPGh.mjs";
2
- import { n as mcp_default, t as mcpModule } from "./mcp-BVefYLQh.mjs";
1
+ import { S as scopesFromContext, _ as resolveMcpCatalog, a as customToolWorkloadIdentity, b as requiredScopesForOperation, c as createWorkloadMcpBoundary, f as McpAccessDeniedError, g as toMcpToolError, h as mcpPublicErrorCode, i as customToolDescriptor, l as createRuntimeScope, m as assertBoundedMcpValue, n as createWorkloadMcpServer, o as executeCustomToolCall, p as McpPublicError, r as customToolAllows, s as authorizeWorkload, t as createMcpServer, u as createWorkloadMcpRelease, v as defaultOperationScope, w as mcpTool, x as scopeGateAllows, y as normalizeRequiredScopes } from "./create-server-5Eo4QP3q.mjs";
2
+ import { n as mcp_default, t as mcpModule } from "./mcp-i0Kqu_bB.mjs";
3
3
  import { t as mcpPlugin } from "./plugin-BZQe1pji.mjs";
4
- import { t as startStdioServer } from "./stdio-CiEieZ1y.mjs";
4
+ import { t as startStdioServer } from "./stdio-37bSPka9.mjs";
5
5
  import { randomUUID } from "node:crypto";
6
6
 
7
7
  //#region src/server/workload-tool-port.ts
@@ -1,4 +1,4 @@
1
- import { C as validateMcpPrincipal, d as getAppMcpRelease, m as assertBoundedMcpValue, t as createMcpServer } from "./create-server-Ty80vPGh.mjs";
1
+ import { C as validateMcpPrincipal, d as getAppMcpRelease, m as assertBoundedMcpValue, t as createMcpServer } from "./create-server-5Eo4QP3q.mjs";
2
2
  import { t as mcpPlugin } from "./plugin-BZQe1pji.mjs";
3
3
  import { route } from "questpie";
4
4
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -1,3 +1,3 @@
1
- import { t as mcpModule } from "../mcp-BVefYLQh.mjs";
1
+ import { t as mcpModule } from "../mcp-i0Kqu_bB.mjs";
2
2
 
3
3
  export { mcpModule };
@@ -1,4 +1,4 @@
1
- import { t as createMcpServer } from "./create-server-Ty80vPGh.mjs";
1
+ import { t as createMcpServer } from "./create-server-5Eo4QP3q.mjs";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
 
4
4
  //#region src/server/stdio.ts
package/dist/stdio.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { t as startStdioServer } from "./stdio-CiEieZ1y.mjs";
1
+ import { t as startStdioServer } from "./stdio-37bSPka9.mjs";
2
2
 
3
3
  export { startStdioServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@questpie/mcp",
3
- "version": "3.28.9",
3
+ "version": "3.28.11",
4
4
  "description": "Model Context Protocol server for QUESTPIE apps, under the app's own access rules",
5
5
  "repository": {
6
6
  "type": "git",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@modelcontextprotocol/sdk": "^1.29.0",
73
- "questpie": "^3.28.9",
73
+ "questpie": "^3.28.11",
74
74
  "zod": "^4.2.1"
75
75
  },
76
76
  "devDependencies": {
@@ -80,6 +80,6 @@
80
80
  "tsdown": "^0.18.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "questpie": "^3.28.9"
83
+ "questpie": "^3.28.11"
84
84
  }
85
85
  }