@wrongstack/mcp 1.0.27 → 1.0.29

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
@@ -45,7 +45,9 @@ function parseMcpBearerChallenge(header, resource) {
45
45
  const resourceMetadata = challengeParameter(parameters, "resource_metadata");
46
46
  if (resourceMetadata) {
47
47
  const metadataUrl = validateMetadataUrl(resourceMetadata);
48
- if (metadataUrl) challenge.resourceMetadataUrl = metadataUrl;
48
+ if (metadataUrl && isSameHost(metadataUrl, resource)) {
49
+ challenge.resourceMetadataUrl = metadataUrl;
50
+ }
49
51
  }
50
52
  const scope = challengeParameter(parameters, "scope");
51
53
  if (scope) {
@@ -194,6 +196,13 @@ function validateMetadataUrl(value) {
194
196
  return void 0;
195
197
  }
196
198
  }
199
+ function isSameHost(candidate, resource) {
200
+ try {
201
+ return unbracket(new URL(candidate).hostname).toLowerCase() === unbracket(new URL(resource).hostname).toLowerCase();
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
197
206
  function record(value, label2) {
198
207
  if (!value || typeof value !== "object" || Array.isArray(value)) {
199
208
  throw new Error(`MCP ${label2} must be an object`);
@@ -1892,7 +1901,7 @@ function validateElicitationContent(fields, content) {
1892
1901
  out[field.name] = [];
1893
1902
  continue;
1894
1903
  }
1895
- if (raw === void 0 || raw === null || raw === "" || Array.isArray(raw) && raw.length === 0) {
1904
+ if (raw === void 0 || raw === null || Array.isArray(raw) && raw.length === 0) {
1896
1905
  if (field.required) return { ok: false, error: `${label(field)} is required` };
1897
1906
  continue;
1898
1907
  }
@@ -1999,6 +2008,7 @@ var ServerRequestResponder = class {
1999
2008
  import { ToolError as ToolError2 } from "@wrongstack/core/types";
2000
2009
  var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
2001
2010
  async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
2011
+ const safeCap = typeof maxBytes === "number" && Number.isFinite(maxBytes) && maxBytes >= 0 ? maxBytes : MAX_MCP_HTTP_BODY_BYTES;
2002
2012
  const body = res.body;
2003
2013
  if (!body || typeof body.getReader !== "function") {
2004
2014
  return await res.text();
@@ -2012,13 +2022,13 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
2012
2022
  if (done) break;
2013
2023
  if (!value) continue;
2014
2024
  total += value.byteLength;
2015
- if (total > maxBytes) {
2025
+ if (total > safeCap) {
2016
2026
  await reader.cancel().catch(() => void 0);
2017
2027
  throw new ToolError2({
2018
- message: `MCP response body exceeded ${maxBytes} bytes \u2014 refusing to buffer`,
2028
+ message: `MCP response body exceeded ${safeCap} bytes \u2014 refusing to buffer`,
2019
2029
  code: "TOOL_EXECUTION_FAILED",
2020
2030
  toolName: "mcp_transport",
2021
- context: { maxBytes, received: total }
2031
+ context: { maxBytes: safeCap, received: total }
2022
2032
  });
2023
2033
  }
2024
2034
  chunks.push(value);
@@ -2223,15 +2233,16 @@ function createTimeoutSignal(parent, timeoutMs, hold) {
2223
2233
  } else {
2224
2234
  parent?.addEventListener("abort", onAbort, { once: true });
2225
2235
  }
2236
+ const safeTimeout = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 6e4;
2226
2237
  const onTimeout = () => {
2227
2238
  if (hold?.()) {
2228
- timer = setTimeout(onTimeout, timeoutMs);
2239
+ timer = setTimeout(onTimeout, safeTimeout);
2229
2240
  timer.unref?.();
2230
2241
  return;
2231
2242
  }
2232
- ctrl.abort(new Error(`MCP HTTP request timed out after ${timeoutMs}ms`));
2243
+ ctrl.abort(new Error(`MCP HTTP request timed out after ${safeTimeout}ms`));
2233
2244
  };
2234
- let timer = setTimeout(onTimeout, timeoutMs);
2245
+ let timer = setTimeout(onTimeout, safeTimeout);
2235
2246
  timer.unref?.();
2236
2247
  return {
2237
2248
  signal: ctrl.signal,
@@ -2274,8 +2285,8 @@ var BaseHTTPTransport = class {
2274
2285
  this.headers = { ...opts.headers };
2275
2286
  this.authorizationProvider = opts.authorizationProvider;
2276
2287
  this.authorizationResource = canonicalMcpResource(opts.url);
2277
- this.timeout = opts.startupTimeoutMs ?? 1e4;
2278
- this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
2288
+ this.timeout = typeof opts.startupTimeoutMs === "number" && Number.isFinite(opts.startupTimeoutMs) && opts.startupTimeoutMs > 0 ? opts.startupTimeoutMs : 1e4;
2289
+ this.requestTimeout = typeof opts.requestTimeoutMs === "number" && Number.isFinite(opts.requestTimeoutMs) && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : 6e4;
2279
2290
  if (opts.tls) {
2280
2291
  if (opts.tls.rejectUnauthorized === false) {
2281
2292
  const where = urlOriginForLog(this.url);
@@ -3789,7 +3800,7 @@ var MCPClient = class _MCPClient {
3789
3800
  capabilities: this.serverRequests.capabilities(),
3790
3801
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
3791
3802
  },
3792
- this.opts.startupTimeoutMs ?? 1e4
3803
+ typeof this.opts.startupTimeoutMs === "number" && Number.isFinite(this.opts.startupTimeoutMs) && this.opts.startupTimeoutMs > 0 ? this.opts.startupTimeoutMs : 1e4
3793
3804
  );
3794
3805
  if (initialize.error) {
3795
3806
  this.state = "failed";
@@ -3914,9 +3925,13 @@ var MCPClient = class _MCPClient {
3914
3925
  await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
3915
3926
  this.state = "disconnected";
3916
3927
  }
3917
- request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e4, opts) {
3918
- if (this.sseTransport) return this.sseTransport.request(method, params, timeoutMs, opts);
3919
- if (this.httpTransport) return this.httpTransport.request(method, params, timeoutMs, opts);
3928
+ request(method, params, timeoutMs, opts) {
3929
+ const defaultTimeoutMs = typeof this.opts.requestTimeoutMs === "number" && Number.isFinite(this.opts.requestTimeoutMs) && this.opts.requestTimeoutMs > 0 ? this.opts.requestTimeoutMs : 6e4;
3930
+ const effectiveTimeoutMs = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
3931
+ if (this.sseTransport)
3932
+ return this.sseTransport.request(method, params, effectiveTimeoutMs, opts);
3933
+ if (this.httpTransport)
3934
+ return this.httpTransport.request(method, params, effectiveTimeoutMs, opts);
3920
3935
  const signal = opts?.signal;
3921
3936
  if (signal?.aborted) {
3922
3937
  const err = new Error(`MCP "${this.opts.name}" request "${method}" aborted before send`);
@@ -3946,13 +3961,15 @@ var MCPClient = class _MCPClient {
3946
3961
  };
3947
3962
  const onTimeout = () => {
3948
3963
  if (this.serverRequests.awaitingUser) {
3949
- entry.timer = setTimeout(onTimeout, timeoutMs);
3964
+ entry.timer = setTimeout(onTimeout, effectiveTimeoutMs);
3950
3965
  return;
3951
3966
  }
3952
3967
  this.pending.delete(id);
3953
3968
  detach();
3954
3969
  reject(
3955
- new Error(`MCP "${this.opts.name}" request "${method}" timed out after ${timeoutMs}ms`)
3970
+ new Error(
3971
+ `MCP "${this.opts.name}" request "${method}" timed out after ${effectiveTimeoutMs}ms`
3972
+ )
3956
3973
  );
3957
3974
  };
3958
3975
  const entry = {
@@ -3966,7 +3983,7 @@ var MCPClient = class _MCPClient {
3966
3983
  detach();
3967
3984
  reject(err);
3968
3985
  },
3969
- timer: setTimeout(onTimeout, timeoutMs)
3986
+ timer: setTimeout(onTimeout, effectiveTimeoutMs)
3970
3987
  };
3971
3988
  this.pending.set(id, entry);
3972
3989
  const stdin = this.child?.stdin;
@@ -4607,8 +4624,12 @@ function toUserInputRequest(request3, problem) {
4607
4624
  function answerValue(field, answer) {
4608
4625
  if (!answer || answer.delegated) return void 0;
4609
4626
  switch (field.kind) {
4610
- case "string":
4611
- return answer.text?.trim() ? answer.text : void 0;
4627
+ case "string": {
4628
+ const text = answer.text;
4629
+ if (text === void 0) return void 0;
4630
+ if (!text.trim() && !field.required) return void 0;
4631
+ return text;
4632
+ }
4612
4633
  case "number":
4613
4634
  case "integer":
4614
4635
  return answer.text?.trim() || void 0;
@@ -4695,8 +4716,14 @@ async function readConfig(path2) {
4695
4716
  async function writeConfig(path2, cfg) {
4696
4717
  const raw = JSON.stringify(cfg, null, 2);
4697
4718
  const tmp = `${path2}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`;
4698
- await fs.writeFile(tmp, raw, "utf8");
4719
+ let mode = 384;
4720
+ try {
4721
+ mode = (await fs.stat(path2)).mode & 511;
4722
+ } catch {
4723
+ }
4699
4724
  try {
4725
+ await fs.writeFile(tmp, raw, { encoding: "utf8", mode });
4726
+ await fs.chmod(tmp, mode);
4700
4727
  await fs.rename(tmp, path2);
4701
4728
  } catch (err) {
4702
4729
  await fs.rm(tmp, { force: true });
@@ -5130,52 +5157,59 @@ function healthStateFor(connectionState, operations, enabled = true) {
5130
5157
  function evaluateHealthThresholds(operations, thresholds) {
5131
5158
  if (!thresholds) return [];
5132
5159
  const checks = [];
5133
- if (thresholds.connectionLatencyP95Ms !== void 0 && operations.connectionSamples.length > 0) {
5160
+ const connectionP95 = thresholds.connectionLatencyP95Ms;
5161
+ if (isUsableThreshold(connectionP95) && operations.connectionSamples.length > 0) {
5134
5162
  const value = percentile(
5135
5163
  [...operations.connectionSamples].sort((a, b) => a - b),
5136
5164
  0.95
5137
5165
  );
5138
5166
  checks.push({
5139
5167
  name: "connection-latency-p95",
5140
- passed: value <= thresholds.connectionLatencyP95Ms,
5168
+ passed: value <= connectionP95,
5141
5169
  value,
5142
- threshold: thresholds.connectionLatencyP95Ms
5170
+ threshold: connectionP95
5143
5171
  });
5144
5172
  }
5145
- if (thresholds.discoveryLatencyP95Ms !== void 0 && operations.discoverySamples.length > 0) {
5173
+ const discoveryP95 = thresholds.discoveryLatencyP95Ms;
5174
+ if (isUsableThreshold(discoveryP95) && operations.discoverySamples.length > 0) {
5146
5175
  const value = percentile(
5147
5176
  [...operations.discoverySamples].sort((a, b) => a - b),
5148
5177
  0.95
5149
5178
  );
5150
5179
  checks.push({
5151
5180
  name: "discovery-latency-p95",
5152
- passed: value <= thresholds.discoveryLatencyP95Ms,
5181
+ passed: value <= discoveryP95,
5153
5182
  value,
5154
- threshold: thresholds.discoveryLatencyP95Ms
5183
+ threshold: discoveryP95
5155
5184
  });
5156
5185
  }
5157
- if (thresholds.callLatencyP95Ms !== void 0 && operations.callSamples.length > 0) {
5186
+ const callP95 = thresholds.callLatencyP95Ms;
5187
+ if (isUsableThreshold(callP95) && operations.callSamples.length > 0) {
5158
5188
  const value = percentile(
5159
5189
  [...operations.callSamples].sort((a, b) => a - b),
5160
5190
  0.95
5161
5191
  );
5162
5192
  checks.push({
5163
5193
  name: "call-latency-p95",
5164
- passed: value <= thresholds.callLatencyP95Ms,
5194
+ passed: value <= callP95,
5165
5195
  value,
5166
- threshold: thresholds.callLatencyP95Ms
5196
+ threshold: callP95
5167
5197
  });
5168
5198
  }
5169
- if (thresholds.inFlightCalls !== void 0) {
5199
+ const inFlight = thresholds.inFlightCalls;
5200
+ if (isUsableThreshold(inFlight)) {
5170
5201
  checks.push({
5171
5202
  name: "in-flight-calls",
5172
- passed: operations.peakInFlightCalls <= thresholds.inFlightCalls,
5203
+ passed: operations.peakInFlightCalls <= inFlight,
5173
5204
  value: operations.peakInFlightCalls,
5174
- threshold: thresholds.inFlightCalls
5205
+ threshold: inFlight
5175
5206
  });
5176
5207
  }
5177
5208
  return checks;
5178
5209
  }
5210
+ function isUsableThreshold(value) {
5211
+ return typeof value === "number" && Number.isFinite(value);
5212
+ }
5179
5213
  function applyHealthThresholds(state, checks) {
5180
5214
  if (state !== "healthy") return state;
5181
5215
  return checks.some((c) => !c.passed) ? "degraded" : "healthy";
@@ -6623,6 +6657,7 @@ var MCPServer = class {
6623
6657
  logger;
6624
6658
  resources;
6625
6659
  prompts;
6660
+ instructions;
6626
6661
  inFlightRequests = /* @__PURE__ */ new Map();
6627
6662
  constructor(opts) {
6628
6663
  this.host = opts.host;
@@ -6631,6 +6666,7 @@ var MCPServer = class {
6631
6666
  version: MCP_CONSTANTS.CLIENT_INFO.version
6632
6667
  };
6633
6668
  this.logger = opts.logger;
6669
+ this.instructions = opts.instructions?.trim() || void 0;
6634
6670
  this.resources = structuredClone(opts.resources ?? []);
6635
6671
  this.prompts = structuredClone(opts.prompts ?? []);
6636
6672
  }
@@ -6697,7 +6733,8 @@ var MCPServer = class {
6697
6733
  ...this.resources.length > 0 ? { resources: { subscribe: false, listChanged: false } } : {},
6698
6734
  ...this.prompts.length > 0 ? { prompts: { listChanged: false } } : {}
6699
6735
  },
6700
- serverInfo: this.serverInfo
6736
+ serverInfo: this.serverInfo,
6737
+ ...this.instructions ? { instructions: this.instructions } : {}
6701
6738
  };
6702
6739
  case "ping":
6703
6740
  return {};
@@ -6751,7 +6788,7 @@ var MCPServer = class {
6751
6788
  const input = paramsRecord(params);
6752
6789
  const args = stringRecord(input["arguments"], "prompts/get arguments");
6753
6790
  for (const argument of prompt.arguments ?? []) {
6754
- if (argument.required && args[argument.name] === void 0) {
6791
+ if (argument.required && !Object.hasOwn(args, argument.name)) {
6755
6792
  throw new Error(`Prompt "${name}" requires argument "${argument.name}"`);
6756
6793
  }
6757
6794
  }
@@ -6850,11 +6887,11 @@ function requiredParamString(params, field, method) {
6850
6887
  return value;
6851
6888
  }
6852
6889
  function stringRecord(value, label2) {
6853
- if (value === void 0) return {};
6890
+ if (value === void 0) return /* @__PURE__ */ Object.create(null);
6854
6891
  if (!value || typeof value !== "object" || Array.isArray(value)) {
6855
6892
  throw new Error(`${label2} must be an object`);
6856
6893
  }
6857
- const result = {};
6894
+ const result = /* @__PURE__ */ Object.create(null);
6858
6895
  for (const [key, item] of Object.entries(value)) {
6859
6896
  if (typeof item !== "string") throw new Error(`${label2}.${key} must be a string`);
6860
6897
  result[key] = item;
@@ -6863,7 +6900,7 @@ function stringRecord(value, label2) {
6863
6900
  }
6864
6901
  function renderPromptTemplate(template, args) {
6865
6902
  return template.replace(/\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}/g, (_match, name) => {
6866
- const value = args[name];
6903
+ const value = Object.hasOwn(args, name) ? args[name] : void 0;
6867
6904
  if (value === void 0) throw new Error(`Missing prompt template argument "${name}"`);
6868
6905
  return value;
6869
6906
  });
@@ -6902,6 +6939,7 @@ function serveStdio(server, opts = {}) {
6902
6939
  let bufferBytes = 0;
6903
6940
  let closed = false;
6904
6941
  let bufferTooLarge = false;
6942
+ const decoder = new TextDecoder();
6905
6943
  let writeChain = Promise.resolve();
6906
6944
  const inFlightHandlers = /* @__PURE__ */ new Set();
6907
6945
  const writeLine = (s) => {
@@ -6924,7 +6962,7 @@ function serveStdio(server, opts = {}) {
6924
6962
  };
6925
6963
  const onData = (chunk) => {
6926
6964
  if (bufferTooLarge) return;
6927
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
6965
+ const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
6928
6966
  buffer += text;
6929
6967
  bufferBytes += Buffer.byteLength(text, "utf8");
6930
6968
  if (bufferBytes > HTTP_BODY_CAP) {
@@ -7215,8 +7253,8 @@ var MCPVaultTokenStore = class {
7215
7253
  async readFile() {
7216
7254
  let raw;
7217
7255
  try {
7218
- const stat2 = await fs3.stat(this.filePath);
7219
- if (stat2.size > MAX_STORE_BYTES) throw new Error("MCP token store exceeds size limit");
7256
+ const stat3 = await fs3.stat(this.filePath);
7257
+ if (stat3.size > MAX_STORE_BYTES) throw new Error("MCP token store exceeds size limit");
7220
7258
  raw = await fs3.readFile(this.filePath, "utf8");
7221
7259
  } catch (error) {
7222
7260
  if (error.code === "ENOENT") return emptyFile();
package/dist/server.d.ts CHANGED
@@ -67,6 +67,11 @@ export interface MCPServerOptions {
67
67
  resources?: MCPServerResource[] | undefined;
68
68
  /** Explicit allowlist only; omitted means this server exposes no prompts. */
69
69
  prompts?: MCPServerPrompt[] | undefined;
70
+ /**
71
+ * `InitializeResult.instructions`: how and when to use this server. Clients
72
+ * that honour it (Claude Code does) put it in front of the model.
73
+ */
74
+ instructions?: string | undefined;
70
75
  }
71
76
  export declare class MCPServer {
72
77
  private readonly host;
@@ -74,6 +79,7 @@ export declare class MCPServer {
74
79
  private readonly logger?;
75
80
  private readonly resources;
76
81
  private readonly prompts;
82
+ private readonly instructions;
77
83
  private readonly inFlightRequests;
78
84
  constructor(opts: MCPServerOptions);
79
85
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Model Context Protocol client and registry: stdio, SSE, and streamable HTTP transports.",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "!dist/**/*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@wrongstack/core": "1.0.27",
29
+ "@wrongstack/core": "1.0.29",
30
30
  "undici": "^8.10.2"
31
31
  },
32
32
  "devDependencies": {