@yawlabs/mcp-compliance 0.13.2 → 0.13.4

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.
@@ -1447,6 +1447,7 @@ async function runComplianceSuite(target, options = {}) {
1447
1447
  }
1448
1448
  const nextId = createIdCounter(1e3);
1449
1449
  const timeout = options.timeout || 15e3;
1450
+ const startupTimeout = options.startupTimeout ?? Math.max(timeout, 6e4);
1450
1451
  const retries = options.retries || 0;
1451
1452
  let sessionId = null;
1452
1453
  let negotiatedProtocolVersion = null;
@@ -1702,16 +1703,24 @@ async function runComplianceSuite(target, options = {}) {
1702
1703
  }
1703
1704
  );
1704
1705
  let initRes = null;
1706
+ const initStart = Date.now();
1705
1707
  try {
1706
- initRes = await rpc("initialize", {
1707
- protocolVersion: SPEC_VERSION,
1708
- capabilities: {
1709
- sampling: {},
1710
- roots: { listChanged: true },
1711
- elicitation: {}
1708
+ initRes = await mcpRequest(
1709
+ backendUrl,
1710
+ "initialize",
1711
+ {
1712
+ protocolVersion: SPEC_VERSION,
1713
+ capabilities: {
1714
+ sampling: {},
1715
+ roots: { listChanged: true },
1716
+ elicitation: {}
1717
+ },
1718
+ clientInfo: { name: "mcp-compliance", version: TOOL_VERSION }
1712
1719
  },
1713
- clientInfo: { name: "mcp-compliance", version: TOOL_VERSION }
1714
- });
1720
+ nextId,
1721
+ buildHeaders2(),
1722
+ startupTimeout
1723
+ );
1715
1724
  const result = initRes?.body?.result;
1716
1725
  if (result) {
1717
1726
  serverInfo.protocolVersion = result.protocolVersion || null;
@@ -1730,8 +1739,14 @@ async function runComplianceSuite(target, options = {}) {
1730
1739
  }
1731
1740
  } catch {
1732
1741
  }
1742
+ const initElapsed = Date.now() - initStart;
1743
+ if (initRes && initElapsed > timeout) {
1744
+ warnings.push(
1745
+ `Initialize handshake took ${initElapsed}ms \u2014 longer than --timeout ${timeout}ms. Per-test requests use --timeout, so slow servers may flake. Consider raising --timeout.`
1746
+ );
1747
+ }
1733
1748
  try {
1734
- await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders2(), timeout);
1749
+ await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders2(), startupTimeout);
1735
1750
  } catch {
1736
1751
  }
1737
1752
  const hasTools = !!serverInfo.capabilities.tools;
package/dist/index.js CHANGED
@@ -559,6 +559,7 @@ function validate(raw, source) {
559
559
  const allowed = /* @__PURE__ */ new Set([
560
560
  "target",
561
561
  "timeout",
562
+ "startupTimeout",
562
563
  "preflightTimeout",
563
564
  "retries",
564
565
  "only",
@@ -1803,6 +1804,7 @@ async function runComplianceSuite(target, options = {}) {
1803
1804
  }
1804
1805
  const nextId = createIdCounter(1e3);
1805
1806
  const timeout = options.timeout || 15e3;
1807
+ const startupTimeout = options.startupTimeout ?? Math.max(timeout, 6e4);
1806
1808
  const retries = options.retries || 0;
1807
1809
  let sessionId = null;
1808
1810
  let negotiatedProtocolVersion = null;
@@ -2058,16 +2060,24 @@ async function runComplianceSuite(target, options = {}) {
2058
2060
  }
2059
2061
  );
2060
2062
  let initRes = null;
2063
+ const initStart = Date.now();
2061
2064
  try {
2062
- initRes = await rpc("initialize", {
2063
- protocolVersion: SPEC_VERSION,
2064
- capabilities: {
2065
- sampling: {},
2066
- roots: { listChanged: true },
2067
- elicitation: {}
2065
+ initRes = await mcpRequest(
2066
+ backendUrl,
2067
+ "initialize",
2068
+ {
2069
+ protocolVersion: SPEC_VERSION,
2070
+ capabilities: {
2071
+ sampling: {},
2072
+ roots: { listChanged: true },
2073
+ elicitation: {}
2074
+ },
2075
+ clientInfo: { name: "mcp-compliance", version: TOOL_VERSION }
2068
2076
  },
2069
- clientInfo: { name: "mcp-compliance", version: TOOL_VERSION }
2070
- });
2077
+ nextId,
2078
+ buildHeaders2(),
2079
+ startupTimeout
2080
+ );
2071
2081
  const result = initRes?.body?.result;
2072
2082
  if (result) {
2073
2083
  serverInfo.protocolVersion = result.protocolVersion || null;
@@ -2086,8 +2096,14 @@ async function runComplianceSuite(target, options = {}) {
2086
2096
  }
2087
2097
  } catch {
2088
2098
  }
2099
+ const initElapsed = Date.now() - initStart;
2100
+ if (initRes && initElapsed > timeout) {
2101
+ warnings.push(
2102
+ `Initialize handshake took ${initElapsed}ms \u2014 longer than --timeout ${timeout}ms. Per-test requests use --timeout, so slow servers may flake. Consider raising --timeout.`
2103
+ );
2104
+ }
2089
2105
  try {
2090
- await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders2(), timeout);
2106
+ await mcpNotification(backendUrl, "notifications/initialized", void 0, buildHeaders2(), startupTimeout);
2091
2107
  } catch {
2092
2108
  }
2093
2109
  const hasTools = !!serverInfo.capabilities.tools;
@@ -5161,6 +5177,20 @@ function formatHtml(report) {
5161
5177
  </html>`;
5162
5178
  }
5163
5179
 
5180
+ // src/stdio-split.ts
5181
+ function splitStdioTarget(s) {
5182
+ if (/["'`]/.test(s)) {
5183
+ throw new Error(
5184
+ "stdio command contains quote characters \u2014 pass the command and its args as separate tokens (e.g. `mcp-compliance test node dist/index.js serve`) instead of wrapping them in one quoted string."
5185
+ );
5186
+ }
5187
+ const tokens = s.trim().split(/\s+/).filter(Boolean);
5188
+ if (tokens.length === 0) {
5189
+ throw new Error("stdio command is empty");
5190
+ }
5191
+ return { command: tokens[0], args: tokens.slice(1) };
5192
+ }
5193
+
5164
5194
  // src/token-store.ts
5165
5195
  import { createHash as createHash2 } from "crypto";
5166
5196
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
@@ -5290,10 +5320,17 @@ function buildTarget(positional, extraArgs, opts) {
5290
5320
  ...opts.envFile ? readEnvFile(opts.envFile) : {},
5291
5321
  ...opts.env ?? {}
5292
5322
  };
5323
+ let command = positional;
5324
+ let args = extraArgs;
5325
+ if (extraArgs.length === 0 && /\s/.test(positional)) {
5326
+ const split = splitStdioTarget(positional);
5327
+ command = split.command;
5328
+ args = split.args;
5329
+ }
5293
5330
  return {
5294
5331
  type: "stdio",
5295
- command: positional,
5296
- args: extraArgs,
5332
+ command,
5333
+ args,
5297
5334
  env: Object.keys(env).length ? env : void 0,
5298
5335
  cwd: opts.cwd,
5299
5336
  verbose: opts.verbose
@@ -5383,8 +5420,11 @@ program.command("test").description("Run the full compliance test suite against
5383
5420
  {}
5384
5421
  ).option("--auth <token>", 'Shorthand for -H "Authorization: <token>" (HTTP only)').option("-E, --env <var>", 'Set env var for stdio command ("KEY=VALUE", repeatable)', parseEnvVar, {}).option("--env-file <path>", "Load env vars from file (KEY=VALUE per line, stdio only)").option("--cwd <dir>", "Working directory for stdio command").option(
5385
5422
  "--timeout <ms>",
5386
- "Request timeout in milliseconds (bump to 30000+ for stdio servers with slow startup)",
5423
+ "Per-request timeout in milliseconds (applies to every test request after the initial handshake)",
5387
5424
  "15000"
5425
+ ).option(
5426
+ "--startup-timeout <ms>",
5427
+ "Deadline for the initial initialize handshake (default: max(--timeout, 60000); covers cold `npx` cache fetches before a stdio server starts)"
5388
5428
  ).option("--no-color", "Disable colored output (also honors NO_COLOR env var)").option("--watch", "Re-run tests when files in the cwd change (stdio targets only)").option(
5389
5429
  "--concurrency <n>",
5390
5430
  "Max parallel-safe tests in flight (default 1; see docs/PERFORMANCE.md before raising)",
@@ -5446,6 +5486,7 @@ Testing ${describeTarget(transportTarget)}...
5446
5486
  }
5447
5487
  const report2 = await runComplianceSuite(transportTarget, {
5448
5488
  timeout: parsePositiveInt(opts.timeout, "--timeout", 1),
5489
+ startupTimeout: opts.startupTimeout ? parsePositiveInt(opts.startupTimeout, "--startup-timeout", 1) : config?.startupTimeout,
5449
5490
  preflightTimeout: opts.preflightTimeout ? parsePositiveInt(opts.preflightTimeout, "--preflight-timeout", 1) : config?.preflightTimeout,
5450
5491
  retries: parsePositiveInt(opts.retries, "--retries"),
5451
5492
  concurrency: parsePositiveInt(opts.concurrency, "--concurrency", 1),
@@ -2,7 +2,7 @@ import {
2
2
  SPEC_BASE,
3
3
  TEST_DEFINITIONS,
4
4
  runComplianceSuite
5
- } from "../chunk-X5CVUDPW.js";
5
+ } from "../chunk-6PF56RRO.js";
6
6
 
7
7
  // src/mcp/server.ts
8
8
  import { existsSync, readFileSync, realpathSync } from "fs";
package/dist/runner.d.ts CHANGED
@@ -180,6 +180,18 @@ interface RunOptions {
180
180
  headers?: Record<string, string>;
181
181
  /** Request timeout in milliseconds (default: 15000) */
182
182
  timeout?: number;
183
+ /**
184
+ * Deadline for the initial `initialize` handshake + `initialized`
185
+ * notification, in milliseconds. Kept separate from `timeout` because
186
+ * cold-started stdio servers — especially `npx @pkg ...` targets where
187
+ * npm has to resolve and fetch the package before the MCP server
188
+ * starts — can take tens of seconds to produce their first response,
189
+ * while steady-state requests complete in milliseconds. Default is
190
+ * `max(timeout, 60000)` so users who bump `--timeout` keep that value
191
+ * and users on defaults get 60s for startup. Does not apply to any
192
+ * per-test requests past the handshake.
193
+ */
194
+ startupTimeout?: number;
183
195
  /** Number of retries for failed tests (default: 0) */
184
196
  retries?: number;
185
197
  /** Only run tests matching these category names or test IDs */
package/dist/runner.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  previewTests,
11
11
  runComplianceSuite,
12
12
  urlHash
13
- } from "./chunk-X5CVUDPW.js";
13
+ } from "./chunk-6PF56RRO.js";
14
14
  export {
15
15
  SPEC_BASE,
16
16
  SPEC_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-compliance",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "CLI tool and MCP server that tests MCP servers for spec compliance",
5
5
  "license": "MIT",
6
6
  "author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",