@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 +78 -40
- package/dist/server.d.ts +6 -0
- package/package.json +2 -2
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
|
|
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 ||
|
|
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 >
|
|
2025
|
+
if (total > safeCap) {
|
|
2016
2026
|
await reader.cancel().catch(() => void 0);
|
|
2017
2027
|
throw new ToolError2({
|
|
2018
|
-
message: `MCP response body exceeded ${
|
|
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,
|
|
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 ${
|
|
2243
|
+
ctrl.abort(new Error(`MCP HTTP request timed out after ${safeTimeout}ms`));
|
|
2233
2244
|
};
|
|
2234
|
-
let timer = setTimeout(onTimeout,
|
|
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
|
|
2278
|
-
this.requestTimeout = opts.requestTimeoutMs
|
|
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
|
|
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
|
|
3918
|
-
|
|
3919
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <=
|
|
5168
|
+
passed: value <= connectionP95,
|
|
5141
5169
|
value,
|
|
5142
|
-
threshold:
|
|
5170
|
+
threshold: connectionP95
|
|
5143
5171
|
});
|
|
5144
5172
|
}
|
|
5145
|
-
|
|
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 <=
|
|
5181
|
+
passed: value <= discoveryP95,
|
|
5153
5182
|
value,
|
|
5154
|
-
threshold:
|
|
5183
|
+
threshold: discoveryP95
|
|
5155
5184
|
});
|
|
5156
5185
|
}
|
|
5157
|
-
|
|
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 <=
|
|
5194
|
+
passed: value <= callP95,
|
|
5165
5195
|
value,
|
|
5166
|
-
threshold:
|
|
5196
|
+
threshold: callP95
|
|
5167
5197
|
});
|
|
5168
5198
|
}
|
|
5169
|
-
|
|
5199
|
+
const inFlight = thresholds.inFlightCalls;
|
|
5200
|
+
if (isUsableThreshold(inFlight)) {
|
|
5170
5201
|
checks.push({
|
|
5171
5202
|
name: "in-flight-calls",
|
|
5172
|
-
passed: operations.peakInFlightCalls <=
|
|
5203
|
+
passed: operations.peakInFlightCalls <= inFlight,
|
|
5173
5204
|
value: operations.peakInFlightCalls,
|
|
5174
|
-
threshold:
|
|
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
|
|
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 :
|
|
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
|
|
7219
|
-
if (
|
|
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.
|
|
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.
|
|
29
|
+
"@wrongstack/core": "1.0.29",
|
|
30
30
|
"undici": "^8.10.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|