@wrongstack/mcp 1.0.17 → 1.0.24

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.
@@ -0,0 +1,15 @@
1
+ import type { MCPPageOptions, MCPRequestOptions } from './client-types.js';
2
+ import { type MCPGetPromptResult, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPReadResourceResult } from './protocol.js';
3
+ export declare class MCPCapabilityClient {
4
+ private readonly requestCapability;
5
+ private readonly requireResourceSubscriptions;
6
+ constructor(requestCapability: <T>(capability: 'resources' | 'prompts', method: string, params: unknown, parse: (value: unknown) => T, opts: MCPRequestOptions) => Promise<T>, requireResourceSubscriptions: (method: string) => void);
7
+ listResources(opts?: MCPPageOptions): Promise<MCPListResourcesResult>;
8
+ listResourceTemplates(opts?: MCPPageOptions): Promise<MCPListResourceTemplatesResult>;
9
+ readResource(uri: string, opts?: MCPRequestOptions): Promise<MCPReadResourceResult>;
10
+ subscribeResource(uri: string, opts?: MCPRequestOptions): Promise<void>;
11
+ unsubscribeResource(uri: string, opts?: MCPRequestOptions): Promise<void>;
12
+ listPrompts(opts?: MCPPageOptions): Promise<MCPListPromptsResult>;
13
+ getPrompt(name: string, args?: Record<string, string> | undefined, opts?: MCPRequestOptions): Promise<MCPGetPromptResult>;
14
+ }
15
+ //# sourceMappingURL=client-capabilities.d.ts.map
@@ -1,4 +1,10 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
+ /** Resolve an HTTP Bearer token without ever persisting the secret itself. */
3
+ export declare function resolveHttpBearerHeaders(options: {
4
+ headers?: Record<string, string> | undefined;
5
+ bearerTokenEnv?: string | undefined;
6
+ name: string;
7
+ }, env?: NodeJS.ProcessEnv): Record<string, string> | undefined;
2
8
  /**
3
9
  * Force-kill a child and its descendants. On Windows a stdio server is launched
4
10
  * through a `.cmd` shim with `shell: true`, so `child` is the `cmd.exe` wrapper
@@ -0,0 +1,62 @@
1
+ import type { MCPAuthorizationProvider } from './authorization.js';
2
+ import type { MCPTool } from './contracts.js';
3
+ export type Transport = 'stdio' | 'sse' | 'streamable-http';
4
+ export interface MCPClientOptions {
5
+ name: string;
6
+ transport: Transport;
7
+ command?: string | undefined;
8
+ args?: string[] | undefined;
9
+ env?: Record<string, string> | undefined;
10
+ url?: string | undefined;
11
+ headers?: Record<string, string> | undefined;
12
+ /** Resolve an HTTP Bearer token from this environment variable at connect time. */
13
+ bearerTokenEnv?: string | undefined;
14
+ startupTimeoutMs?: number | undefined;
15
+ requestTimeoutMs?: number | undefined;
16
+ /**
17
+ * Working directory for a stdio server process. Presets address the project
18
+ * as `--project-root .` / `server-filesystem .`, which resolve against the
19
+ * spawn cwd — without this the child inherited WrongStack's PROCESS cwd, so
20
+ * a host serving a project from elsewhere (WebUI, ACP sessions with their
21
+ * own cwd) pointed those servers at the wrong directory.
22
+ */
23
+ cwd?: string | undefined;
24
+ /** Host-owned, vault-backed authorization for HTTP transports. */
25
+ authorizationProvider?: MCPAuthorizationProvider | undefined;
26
+ /**
27
+ * Allowlist of env var names to forward from the parent process (process.env)
28
+ * to the child. Values are resolved at spawn time and merged into `env`
29
+ * via the `extra` path of `buildChildEnv` (unfiltered). This is how built-in
30
+ * MCP server presets (GitHub, Slack, Brave Search, …) get their API tokens
31
+ * without storing them in config.json or being scrubbed by the secret filter.
32
+ */
33
+ passthroughEnv?: string[] | undefined;
34
+ /**
35
+ * Resolution-bound private-network policy for HTTP transports. Default:
36
+ * private/LAN targets are blocked at dial time (DNS-rebinding safe); the
37
+ * flag opts this server in. See MCPServerConfig.allowPrivateNetworks.
38
+ */
39
+ allowPrivateNetworks?: boolean | undefined;
40
+ }
41
+ export interface MCPRequestOptions {
42
+ signal?: AbortSignal | undefined;
43
+ }
44
+ export interface MCPPageOptions extends MCPRequestOptions {
45
+ cursor?: string | undefined;
46
+ }
47
+ export interface JsonRpcRequest {
48
+ jsonrpc: '2.0';
49
+ id: number;
50
+ method: string;
51
+ params?: unknown | undefined;
52
+ }
53
+ export type JsonRpcServerRequest = {
54
+ jsonrpc: '2.0';
55
+ id: number | string;
56
+ method: string;
57
+ params?: unknown | undefined;
58
+ };
59
+ export type ExitListener = (name: string, code: number | null, signal: string | null) => void;
60
+ export type ToolsChangedListener = (name: string, tools: MCPTool[]) => void;
61
+ export type MCPListChangedListener = (name: string) => void;
62
+ //# sourceMappingURL=client-types.d.ts.map
package/dist/client.d.ts CHANGED
@@ -1,56 +1,12 @@
1
- import type { MCPAuthorizationProvider } from './authorization.js';
1
+ import type { ExitListener, MCPClientOptions, MCPListChangedListener, MCPPageOptions, MCPRequestOptions, ToolsChangedListener } from './client-types.js';
2
+ import type { ConnectionState, JsonRpcResponse, MCPTool, ToolCallResult } from './contracts.js';
2
3
  import { type MCPGetPromptResult, type MCPListPromptsResult, type MCPListResourcesResult, type MCPListResourceTemplatesResult, type MCPReadResourceResult, type MCPServerMetadata } from './protocol.js';
3
4
  export { forceKillTree } from './client-process.js';
4
5
  export { quoteWindowsArg } from './client-protocol-helpers.js';
5
- export type Transport = 'stdio' | 'sse' | 'streamable-http';
6
- export interface MCPClientOptions {
7
- name: string;
8
- transport: Transport;
9
- command?: string | undefined;
10
- args?: string[] | undefined;
11
- env?: Record<string, string> | undefined;
12
- url?: string | undefined;
13
- headers?: Record<string, string> | undefined;
14
- startupTimeoutMs?: number | undefined;
15
- requestTimeoutMs?: number | undefined;
16
- /**
17
- * Working directory for a stdio server process. Presets address the project
18
- * as `--project-root .` / `server-filesystem .`, which resolve against the
19
- * spawn cwd — without this the child inherited WrongStack's PROCESS cwd, so
20
- * a host serving a project from elsewhere (WebUI, ACP sessions with their
21
- * own cwd) pointed those servers at the wrong directory.
22
- */
23
- cwd?: string | undefined;
24
- /** Host-owned, vault-backed authorization for HTTP transports. */
25
- authorizationProvider?: MCPAuthorizationProvider | undefined;
26
- /**
27
- * Allowlist of env var names to forward from the parent process (process.env)
28
- * to the child. Values are resolved at spawn time and merged into `env`
29
- * via the `extra` path of `buildChildEnv` (unfiltered). This is how built-in
30
- * MCP server presets (GitHub, Slack, Brave Search, …) get their API tokens
31
- * without storing them in config.json or being scrubbed by the secret filter.
32
- */
33
- passthroughEnv?: string[] | undefined;
34
- /**
35
- * Resolution-bound private-network policy for HTTP transports. Default:
36
- * private/LAN targets are blocked at dial time (DNS-rebinding safe); the
37
- * flag opts this server in. See MCPServerConfig.allowPrivateNetworks.
38
- */
39
- allowPrivateNetworks?: boolean | undefined;
40
- }
41
- import type { ConnectionState, JsonRpcResponse, MCPTool, ToolCallResult } from './contracts.js';
42
6
  export type { ConnectionState, JsonRpcResponse, MCPTool, ToolCallResult };
43
- export interface MCPRequestOptions {
44
- signal?: AbortSignal | undefined;
45
- }
46
- export interface MCPPageOptions extends MCPRequestOptions {
47
- cursor?: string | undefined;
48
- }
49
- type ExitListener = (name: string, code: number | null, signal: string | null) => void;
50
- type ToolsChangedListener = (name: string, tools: MCPTool[]) => void;
51
- export type MCPListChangedListener = (name: string) => void;
52
7
  export declare class MCPClient {
53
8
  readonly opts: MCPClientOptions;
9
+ private readonly capabilityClient;
54
10
  /**
55
11
  * Maximum bytes the rx buffer may accumulate before the connection is
56
12
  * forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
@@ -161,4 +117,5 @@ export declare class MCPClient {
161
117
  removePromptsChangedListener(listener: MCPListChangedListener): void;
162
118
  private emitCapabilityChanged;
163
119
  }
120
+ export type { MCPClientOptions, MCPListChangedListener, MCPPageOptions, MCPRequestOptions, Transport, } from './client-types.js';
164
121
  //# sourceMappingURL=client.d.ts.map
package/dist/index.js CHANGED
@@ -831,40 +831,6 @@ import { spawn as spawn2 } from "node:child_process";
831
831
  import { StringDecoder } from "node:string_decoder";
832
832
  import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMessage } from "@wrongstack/core/utils";
833
833
 
834
- // src/client-process.ts
835
- import { spawn } from "node:child_process";
836
- import { buildChildEnv } from "@wrongstack/core/utils";
837
- function forceKillTree(child) {
838
- if (child.pid === void 0) {
839
- try {
840
- child.kill("SIGKILL");
841
- } catch {
842
- }
843
- return;
844
- }
845
- if (process.platform === "win32") {
846
- const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
847
- stdio: "ignore",
848
- // H-8 convention (spawn-convention test): taskkill needs PATH only —
849
- // do not hand it the credential-bearing parent environment.
850
- env: buildChildEnv(),
851
- windowsHide: true
852
- });
853
- killer.once("error", () => {
854
- try {
855
- child.kill("SIGKILL");
856
- } catch {
857
- }
858
- });
859
- killer.unref();
860
- return;
861
- }
862
- try {
863
- child.kill("SIGKILL");
864
- } catch {
865
- }
866
- }
867
-
868
834
  // src/client-protocol-helpers.ts
869
835
  var MAX_PROTOCOL_INPUT_CHARS = 8192;
870
836
  function validateProtocolString(value, label, allowEmpty = false) {
@@ -886,57 +852,6 @@ function parseEmptyResult(value) {
886
852
  }
887
853
  }
888
854
 
889
- // src/constants.ts
890
- var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2024-11-05"]);
891
- function negotiateProtocolVersion(requested) {
892
- if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
893
- return requested;
894
- }
895
- return MCP_CONSTANTS.PROTOCOL_VERSION;
896
- }
897
- var MCP_CONSTANTS = Object.freeze({
898
- /** MCP protocol version advertised during handshake: the newest we implement. */
899
- PROTOCOL_VERSION: SUPPORTED_PROTOCOL_VERSIONS[0],
900
- /** Identity announced to MCP servers during `initialize`. */
901
- CLIENT_INFO: Object.freeze({
902
- name: "wrongstack",
903
- version: "0.1.10"
904
- }),
905
- /** Reconnection behaviour when a transport disconnects. */
906
- RECONNECT: Object.freeze({
907
- /** Max full reconnect cycles before the slot is marked `failed`. */
908
- MAX_CYCLES: 5,
909
- /** Base delay between cycles (exponential backoff applied on top). */
910
- BASE_DELAY_MS: 1e3,
911
- /** Jitter factor applied to the backoff (0 = no jitter, 1 = full). */
912
- JITTER_FACTOR: 0.2,
913
- /** Max connection attempts within a single cycle. */
914
- MAX_ATTEMPTS: 3,
915
- /** Base multiplier for the exponential backoff formula (`delay = BASE * multiplier^attempt`). */
916
- BACKOFF_MULTIPLIER: 2
917
- }),
918
- /** Timing for graceful / forced disconnect. */
919
- DISCONNECT: Object.freeze({
920
- /** Ms to wait for in-flight requests to complete before force-closing. */
921
- GRACEFUL_MS: 800,
922
- /** Ms after which the force disconnect is triggered. */
923
- FORCE_TIMEOUT_MS: 1200
924
- }),
925
- /** Lazy-connect idle lifecycle. */
926
- IDLE: Object.freeze({
927
- /** Default ms a lazy server stays connected with no tool calls before auto-sleep. */
928
- DEFAULT_TIMEOUT_MS: 3e5,
929
- /** How often the idle sweep runs (kept well below the timeout). */
930
- SWEEP_INTERVAL_MS: 3e4
931
- }),
932
- /** JSON-RPC response timeout for outstanding requests. */
933
- RESPONSE_TIMEOUT_MS: 500,
934
- /** Max buffer size for the SSE reader. */
935
- SSE_READER_MAX_BUFFER: 256 * 1024,
936
- /** Max characters logged from a request body. */
937
- REQUEST_LOG_CAP: 1024
938
- });
939
-
940
855
  // src/protocol.ts
941
856
  function record2(value, label) {
942
857
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -1138,6 +1053,192 @@ function parseGetPromptResult(value) {
1138
1053
  };
1139
1054
  }
1140
1055
 
1056
+ // src/client-capabilities.ts
1057
+ var MCPCapabilityClient = class {
1058
+ constructor(requestCapability, requireResourceSubscriptions) {
1059
+ this.requestCapability = requestCapability;
1060
+ this.requireResourceSubscriptions = requireResourceSubscriptions;
1061
+ }
1062
+ requestCapability;
1063
+ requireResourceSubscriptions;
1064
+ async listResources(opts = {}) {
1065
+ const params = pageParams(opts.cursor, "resources/list cursor");
1066
+ return this.requestCapability(
1067
+ "resources",
1068
+ "resources/list",
1069
+ params,
1070
+ parseListResourcesResult,
1071
+ opts
1072
+ );
1073
+ }
1074
+ async listResourceTemplates(opts = {}) {
1075
+ const params = pageParams(opts.cursor, "resources/templates/list cursor");
1076
+ return this.requestCapability(
1077
+ "resources",
1078
+ "resources/templates/list",
1079
+ params,
1080
+ parseListResourceTemplatesResult,
1081
+ opts
1082
+ );
1083
+ }
1084
+ async readResource(uri, opts = {}) {
1085
+ validateProtocolString(uri, "resource URI");
1086
+ return this.requestCapability(
1087
+ "resources",
1088
+ "resources/read",
1089
+ { uri },
1090
+ parseReadResourceResult,
1091
+ opts
1092
+ );
1093
+ }
1094
+ async subscribeResource(uri, opts = {}) {
1095
+ validateProtocolString(uri, "resource URI");
1096
+ this.requireResourceSubscriptions("resources/subscribe");
1097
+ await this.requestCapability(
1098
+ "resources",
1099
+ "resources/subscribe",
1100
+ { uri },
1101
+ parseEmptyResult,
1102
+ opts
1103
+ );
1104
+ }
1105
+ async unsubscribeResource(uri, opts = {}) {
1106
+ validateProtocolString(uri, "resource URI");
1107
+ this.requireResourceSubscriptions("resources/unsubscribe");
1108
+ await this.requestCapability(
1109
+ "resources",
1110
+ "resources/unsubscribe",
1111
+ { uri },
1112
+ parseEmptyResult,
1113
+ opts
1114
+ );
1115
+ }
1116
+ async listPrompts(opts = {}) {
1117
+ const params = pageParams(opts.cursor, "prompts/list cursor");
1118
+ return this.requestCapability("prompts", "prompts/list", params, parseListPromptsResult, opts);
1119
+ }
1120
+ async getPrompt(name, args, opts = {}) {
1121
+ validateProtocolString(name, "prompt name");
1122
+ if (args && Object.keys(args).length > 64) {
1123
+ throw new Error("MCP prompt arguments exceed the limit of 64");
1124
+ }
1125
+ for (const [key, value] of Object.entries(args ?? {})) {
1126
+ validateProtocolString(key, "prompt argument name");
1127
+ validateProtocolString(value, `prompt argument "${key}"`, true);
1128
+ }
1129
+ return this.requestCapability(
1130
+ "prompts",
1131
+ "prompts/get",
1132
+ args === void 0 ? { name } : { name, arguments: args },
1133
+ parseGetPromptResult,
1134
+ opts
1135
+ );
1136
+ }
1137
+ };
1138
+
1139
+ // src/client-process.ts
1140
+ import { spawn } from "node:child_process";
1141
+ import { buildChildEnv } from "@wrongstack/core/utils";
1142
+ function resolveHttpBearerHeaders(options, env = process.env) {
1143
+ const envName = options.bearerTokenEnv?.trim();
1144
+ if (!envName) return options.headers;
1145
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(envName)) {
1146
+ throw new Error(`MCP bearerTokenEnv "${envName}" is not a valid environment variable name`);
1147
+ }
1148
+ const token = env[envName];
1149
+ if (!token) throw new Error(`MCP "${options.name}" requires environment variable ${envName}`);
1150
+ if (token.length > 16384 || /[\r\n]/.test(token)) {
1151
+ throw new Error(`MCP "${options.name}" bearer token is oversized or contains newlines`);
1152
+ }
1153
+ const resolved = { ...options.headers };
1154
+ for (const key of Object.keys(resolved)) {
1155
+ if (key.toLowerCase() === "authorization") delete resolved[key];
1156
+ }
1157
+ resolved.Authorization = `Bearer ${token}`;
1158
+ return resolved;
1159
+ }
1160
+ function forceKillTree(child) {
1161
+ if (child.pid === void 0) {
1162
+ try {
1163
+ child.kill("SIGKILL");
1164
+ } catch {
1165
+ }
1166
+ return;
1167
+ }
1168
+ if (process.platform === "win32") {
1169
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
1170
+ stdio: "ignore",
1171
+ // H-8 convention (spawn-convention test): taskkill needs PATH only —
1172
+ // do not hand it the credential-bearing parent environment.
1173
+ env: buildChildEnv(),
1174
+ windowsHide: true
1175
+ });
1176
+ killer.once("error", () => {
1177
+ try {
1178
+ child.kill("SIGKILL");
1179
+ } catch {
1180
+ }
1181
+ });
1182
+ killer.unref();
1183
+ return;
1184
+ }
1185
+ try {
1186
+ child.kill("SIGKILL");
1187
+ } catch {
1188
+ }
1189
+ }
1190
+
1191
+ // src/constants.ts
1192
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2024-11-05"]);
1193
+ function negotiateProtocolVersion(requested) {
1194
+ if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
1195
+ return requested;
1196
+ }
1197
+ return MCP_CONSTANTS.PROTOCOL_VERSION;
1198
+ }
1199
+ var MCP_CONSTANTS = Object.freeze({
1200
+ /** MCP protocol version advertised during handshake: the newest we implement. */
1201
+ PROTOCOL_VERSION: SUPPORTED_PROTOCOL_VERSIONS[0],
1202
+ /** Identity announced to MCP servers during `initialize`. */
1203
+ CLIENT_INFO: Object.freeze({
1204
+ name: "wrongstack",
1205
+ version: "0.1.10"
1206
+ }),
1207
+ /** Reconnection behaviour when a transport disconnects. */
1208
+ RECONNECT: Object.freeze({
1209
+ /** Max full reconnect cycles before the slot is marked `failed`. */
1210
+ MAX_CYCLES: 5,
1211
+ /** Base delay between cycles (exponential backoff applied on top). */
1212
+ BASE_DELAY_MS: 1e3,
1213
+ /** Jitter factor applied to the backoff (0 = no jitter, 1 = full). */
1214
+ JITTER_FACTOR: 0.2,
1215
+ /** Max connection attempts within a single cycle. */
1216
+ MAX_ATTEMPTS: 3,
1217
+ /** Base multiplier for the exponential backoff formula (`delay = BASE * multiplier^attempt`). */
1218
+ BACKOFF_MULTIPLIER: 2
1219
+ }),
1220
+ /** Timing for graceful / forced disconnect. */
1221
+ DISCONNECT: Object.freeze({
1222
+ /** Ms to wait for in-flight requests to complete before force-closing. */
1223
+ GRACEFUL_MS: 800,
1224
+ /** Ms after which the force disconnect is triggered. */
1225
+ FORCE_TIMEOUT_MS: 1200
1226
+ }),
1227
+ /** Lazy-connect idle lifecycle. */
1228
+ IDLE: Object.freeze({
1229
+ /** Default ms a lazy server stays connected with no tool calls before auto-sleep. */
1230
+ DEFAULT_TIMEOUT_MS: 3e5,
1231
+ /** How often the idle sweep runs (kept well below the timeout). */
1232
+ SWEEP_INTERVAL_MS: 3e4
1233
+ }),
1234
+ /** JSON-RPC response timeout for outstanding requests. */
1235
+ RESPONSE_TIMEOUT_MS: 500,
1236
+ /** Max buffer size for the SSE reader. */
1237
+ SSE_READER_MAX_BUFFER: 256 * 1024,
1238
+ /** Max characters logged from a request body. */
1239
+ REQUEST_LOG_CAP: 1024
1240
+ });
1241
+
1141
1242
  // src/tool-schema.ts
1142
1243
  var MAX_TOOL_PAGES = 100;
1143
1244
  var MAX_TOOLS = 1e4;
@@ -1331,7 +1432,6 @@ var SSEReader = class {
1331
1432
  // src/transport-base.ts
1332
1433
  import * as https2 from "node:https";
1333
1434
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
1334
- import { Agent as UndiciAgent, fetch as undiciFetch } from "undici";
1335
1435
 
1336
1436
  // src/transport-security.ts
1337
1437
  import * as dns2 from "node:dns/promises";
@@ -1362,19 +1462,9 @@ function validateTransportUrl(rawUrl) {
1362
1462
  const hostname = url.hostname;
1363
1463
  const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1364
1464
  const ipVersion = net2.isIP(host);
1365
- if (ipVersion === 4) {
1366
- const parts = host.split(".").map(Number);
1367
- if (parts[0] === 169 && parts[1] === 254) {
1368
- throw new ConfigError({
1369
- message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1370
- code: "CONFIG_INVALID",
1371
- context: { field: "url", rawUrl, hostname }
1372
- });
1373
- }
1374
- } else if (ipVersion === 6) {
1375
- const lower = host.toLowerCase();
1376
- const linkLocal = /^fe[89ab]/.test(lower);
1377
- if (linkLocal || lower === "fd00:ec2::254") {
1465
+ if (ipVersion !== 0) {
1466
+ const classification = classifyTransportAddress(host, ipVersion);
1467
+ if (classification === "blocked") {
1378
1468
  throw new ConfigError({
1379
1469
  message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1380
1470
  code: "CONFIG_INVALID",
@@ -1383,7 +1473,7 @@ function validateTransportUrl(rawUrl) {
1383
1473
  }
1384
1474
  }
1385
1475
  if (url.protocol === "http:") {
1386
- const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1476
+ const isLoopback = host === "localhost" || ipVersion !== 0 && classifyTransportAddress(host, ipVersion) === "loopback";
1387
1477
  if (!isLoopback) {
1388
1478
  throw new ConfigError({
1389
1479
  message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`,
@@ -1491,6 +1581,14 @@ function transportPinnedLookup(options) {
1491
1581
  // src/transport-base.ts
1492
1582
  var MAX_TRANSPORT_REDIRECTS = 5;
1493
1583
  var nativeGlobalFetch = globalThis.fetch;
1584
+ var undiciModule;
1585
+ var undiciLoad;
1586
+ async function ensureUndici() {
1587
+ if (undiciModule) return undiciModule;
1588
+ undiciLoad ??= import("undici");
1589
+ undiciModule = await undiciLoad;
1590
+ return undiciModule;
1591
+ }
1494
1592
  var pinnedAgents = /* @__PURE__ */ new Set();
1495
1593
  var pinnedAgentsCleanupRegistered = false;
1496
1594
  if (!pinnedAgentsCleanupRegistered) {
@@ -1608,8 +1706,8 @@ var BaseHTTPTransport = class {
1608
1706
  let hopHeaders = headers;
1609
1707
  for (let hop = 0; hop < MAX_TRANSPORT_REDIRECTS; hop++) {
1610
1708
  const fetchOpts = { ...init, headers: hopHeaders, redirect: "manual" };
1611
- this.applyPinnedDispatcher(fetchOpts);
1612
- const res = await this.dispatcherFetch()(currentUrl, fetchOpts);
1709
+ await this.applyPinnedDispatcher(fetchOpts);
1710
+ const res = await (await this.dispatcherFetch())(currentUrl, fetchOpts);
1613
1711
  if (res.status !== 301 && res.status !== 302 && res.status !== 303 && res.status !== 307 && res.status !== 308) {
1614
1712
  return res;
1615
1713
  }
@@ -1704,13 +1802,16 @@ var BaseHTTPTransport = class {
1704
1802
  }
1705
1803
  }
1706
1804
  }
1707
- dispatcherFetch() {
1708
- return globalThis.fetch === nativeGlobalFetch ? undiciFetch : globalThis.fetch;
1805
+ async dispatcherFetch() {
1806
+ if (globalThis.fetch !== nativeGlobalFetch) return globalThis.fetch;
1807
+ const undici = await ensureUndici();
1808
+ return undici.fetch;
1709
1809
  }
1710
- pinnedDispatcher() {
1810
+ async pinnedDispatcher() {
1811
+ const undici = await ensureUndici();
1711
1812
  if (!this.pinnedAgent) {
1712
1813
  const tls = this.tlsOptions;
1713
- this.pinnedAgent = new UndiciAgent({
1814
+ this.pinnedAgent = new undici.Agent({
1714
1815
  allowH2: false,
1715
1816
  connect: {
1716
1817
  ...tls ? { ca: tls.ca, rejectUnauthorized: tls.rejectUnauthorized } : {},
@@ -1724,8 +1825,8 @@ var BaseHTTPTransport = class {
1724
1825
  }
1725
1826
  return this.pinnedAgent;
1726
1827
  }
1727
- applyPinnedDispatcher(fetchOpts) {
1728
- fetchOpts.dispatcher = this.pinnedDispatcher();
1828
+ async applyPinnedDispatcher(fetchOpts) {
1829
+ fetchOpts.dispatcher = await this.pinnedDispatcher();
1729
1830
  }
1730
1831
  /**
1731
1832
  * Destroy this transport's pinned Agent and its connection pool. Idempotent.
@@ -2525,6 +2626,10 @@ var MCPClient = class _MCPClient {
2525
2626
  this.opts = opts;
2526
2627
  }
2527
2628
  opts;
2629
+ capabilityClient = new MCPCapabilityClient(
2630
+ this.requestCapability.bind(this),
2631
+ this.requireResourceSubscriptions.bind(this)
2632
+ );
2528
2633
  /**
2529
2634
  * Maximum bytes the rx buffer may accumulate before the connection is
2530
2635
  * forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
@@ -2750,7 +2855,7 @@ var MCPClient = class _MCPClient {
2750
2855
  const httpOpts = {
2751
2856
  name: this.opts.name,
2752
2857
  url: this.opts.url,
2753
- headers: this.opts.headers,
2858
+ headers: resolveHttpBearerHeaders(this.opts),
2754
2859
  startupTimeoutMs: this.opts.startupTimeoutMs,
2755
2860
  requestTimeoutMs: this.opts.requestTimeoutMs,
2756
2861
  authorizationProvider: this.opts.authorizationProvider,
@@ -2801,7 +2906,7 @@ var MCPClient = class _MCPClient {
2801
2906
  const httpOpts = {
2802
2907
  name: this.opts.name,
2803
2908
  url: this.opts.url,
2804
- headers: this.opts.headers,
2909
+ headers: resolveHttpBearerHeaders(this.opts),
2805
2910
  startupTimeoutMs: this.opts.startupTimeoutMs,
2806
2911
  requestTimeoutMs: this.opts.requestTimeoutMs,
2807
2912
  authorizationProvider: this.opts.authorizationProvider,
@@ -2865,77 +2970,25 @@ var MCPClient = class _MCPClient {
2865
2970
  };
2866
2971
  }
2867
2972
  async listResources(opts = {}) {
2868
- const params = pageParams(opts.cursor, "resources/list cursor");
2869
- return this.requestCapability(
2870
- "resources",
2871
- "resources/list",
2872
- params,
2873
- parseListResourcesResult,
2874
- opts
2875
- );
2973
+ return this.capabilityClient.listResources(opts);
2876
2974
  }
2877
2975
  async listResourceTemplates(opts = {}) {
2878
- const params = pageParams(opts.cursor, "resources/templates/list cursor");
2879
- return this.requestCapability(
2880
- "resources",
2881
- "resources/templates/list",
2882
- params,
2883
- parseListResourceTemplatesResult,
2884
- opts
2885
- );
2976
+ return this.capabilityClient.listResourceTemplates(opts);
2886
2977
  }
2887
2978
  async readResource(uri, opts = {}) {
2888
- validateProtocolString(uri, "resource URI");
2889
- return this.requestCapability(
2890
- "resources",
2891
- "resources/read",
2892
- { uri },
2893
- parseReadResourceResult,
2894
- opts
2895
- );
2979
+ return this.capabilityClient.readResource(uri, opts);
2896
2980
  }
2897
2981
  async subscribeResource(uri, opts = {}) {
2898
- validateProtocolString(uri, "resource URI");
2899
- this.requireResourceSubscriptions("resources/subscribe");
2900
- await this.requestCapability(
2901
- "resources",
2902
- "resources/subscribe",
2903
- { uri },
2904
- parseEmptyResult,
2905
- opts
2906
- );
2982
+ return this.capabilityClient.subscribeResource(uri, opts);
2907
2983
  }
2908
2984
  async unsubscribeResource(uri, opts = {}) {
2909
- validateProtocolString(uri, "resource URI");
2910
- this.requireResourceSubscriptions("resources/unsubscribe");
2911
- await this.requestCapability(
2912
- "resources",
2913
- "resources/unsubscribe",
2914
- { uri },
2915
- parseEmptyResult,
2916
- opts
2917
- );
2985
+ return this.capabilityClient.unsubscribeResource(uri, opts);
2918
2986
  }
2919
2987
  async listPrompts(opts = {}) {
2920
- const params = pageParams(opts.cursor, "prompts/list cursor");
2921
- return this.requestCapability("prompts", "prompts/list", params, parseListPromptsResult, opts);
2988
+ return this.capabilityClient.listPrompts(opts);
2922
2989
  }
2923
2990
  async getPrompt(name, args, opts = {}) {
2924
- validateProtocolString(name, "prompt name");
2925
- if (args && Object.keys(args).length > 64) {
2926
- throw new Error("MCP prompt arguments exceed the limit of 64");
2927
- }
2928
- for (const [key, value] of Object.entries(args ?? {})) {
2929
- validateProtocolString(key, "prompt argument name");
2930
- validateProtocolString(value, `prompt argument "${key}"`, true);
2931
- }
2932
- return this.requestCapability(
2933
- "prompts",
2934
- "prompts/get",
2935
- args === void 0 ? { name } : { name, arguments: args },
2936
- parseGetPromptResult,
2937
- opts
2938
- );
2991
+ return this.capabilityClient.getPrompt(name, args, opts);
2939
2992
  }
2940
2993
  async close() {
2941
2994
  if (this.closePromise) return this.closePromise;
@@ -3423,11 +3476,25 @@ function requireIdentity(value, label) {
3423
3476
  import { randomBytes as randomBytes3 } from "node:crypto";
3424
3477
  import * as fs from "node:fs/promises";
3425
3478
  async function readConfig(path2) {
3479
+ let raw;
3426
3480
  try {
3427
- return JSON.parse(await fs.readFile(path2, "utf8"));
3481
+ raw = await fs.readFile(path2, "utf8");
3482
+ } catch (error) {
3483
+ if (error.code === "ENOENT") return {};
3484
+ throw error;
3485
+ }
3486
+ let parsed;
3487
+ try {
3488
+ parsed = JSON.parse(raw);
3428
3489
  } catch {
3429
- return {};
3490
+ throw new Error(
3491
+ `Profile config "${path2}" is not valid JSON \u2014 fix or remove it before changing MCP servers`
3492
+ );
3493
+ }
3494
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3495
+ throw new Error(`Profile config "${path2}" must contain a JSON object`);
3430
3496
  }
3497
+ return parsed;
3431
3498
  }
3432
3499
  async function writeConfig(path2, cfg) {
3433
3500
  const raw = JSON.stringify(cfg, null, 2);
@@ -3499,6 +3566,8 @@ function buildConfig(input, base) {
3499
3566
  if (url !== void 0) cfg.url = url;
3500
3567
  const headers = input.headers ?? base?.headers;
3501
3568
  if (headers !== void 0) cfg.headers = headers;
3569
+ const bearerTokenEnv = input.bearerTokenEnv ?? base?.bearerTokenEnv;
3570
+ if (bearerTokenEnv !== void 0) cfg.bearerTokenEnv = bearerTokenEnv;
3502
3571
  const allowedTools = input.allowedTools ?? base?.allowedTools;
3503
3572
  if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
3504
3573
  const permission = input.permission ?? base?.permission;
@@ -3743,6 +3812,7 @@ function manifestConfigHash(cfg) {
3743
3812
  url: cfg.url ?? null,
3744
3813
  env: sortedEntries(cfg.env),
3745
3814
  headers: sortedEntries(cfg.headers),
3815
+ bearerTokenEnv: cfg.bearerTokenEnv ?? null,
3746
3816
  passthroughEnv: passthroughEntries(cfg.passthroughEnv)
3747
3817
  });
3748
3818
  return createHash2("sha256").update(basis).digest("hex").slice(0, 16);
@@ -4060,7 +4130,7 @@ function renderContentBlock(item) {
4060
4130
  if (type === "image" || type === "audio") {
4061
4131
  const mime = typeof item["mimeType"] === "string" ? item["mimeType"] : "unknown type";
4062
4132
  const data = typeof item["data"] === "string" ? item["data"] : "";
4063
- const kb = Math.max(1, Math.round(data.length * 3 / 4 / 1024));
4133
+ const kb = data.length === 0 ? 0 : Math.max(1, Math.round(data.length * 3 / 4 / 1024));
4064
4134
  return `[${type} content: ${mime}, ~${kb} KB \u2014 binary payload not inlined]`;
4065
4135
  }
4066
4136
  if (type === "resource" && item["resource"] && typeof item["resource"] === "object") {
@@ -4077,6 +4147,9 @@ ${resource["text"]}`;
4077
4147
  if (type === "resource_link" && typeof item["uri"] === "string") {
4078
4148
  return `[resource link: ${item["uri"]}]`;
4079
4149
  }
4150
+ if ("text" in item) {
4151
+ return String(item["text"] ?? "");
4152
+ }
4080
4153
  return JSON.stringify(item);
4081
4154
  }
4082
4155
  function stringify(c) {
@@ -4090,10 +4163,7 @@ function stringify(c) {
4090
4163
  }).join("\n");
4091
4164
  }
4092
4165
  if (c && typeof c === "object") {
4093
- if ("text" in c) {
4094
- return String(c.text);
4095
- }
4096
- return JSON.stringify(c);
4166
+ return renderContentBlock(c);
4097
4167
  }
4098
4168
  return String(c ?? "");
4099
4169
  }
@@ -4240,6 +4310,7 @@ async function attemptConnectSlot(ctx, slot) {
4240
4310
  env: slot.cfg.env,
4241
4311
  url: slot.cfg.url,
4242
4312
  headers: slot.cfg.headers,
4313
+ bearerTokenEnv: slot.cfg.bearerTokenEnv,
4243
4314
  startupTimeoutMs: slot.cfg.startupTimeoutMs,
4244
4315
  requestTimeoutMs: slot.cfg.requestTimeoutMs,
4245
4316
  cwd: ctx.cwd,
@@ -4451,8 +4522,10 @@ async function sleepIdleSlot(ctx, slot) {
4451
4522
  clearTimeout(slot.reconnectTimer);
4452
4523
  slot.reconnectTimer = void 0;
4453
4524
  }
4454
- if (slot.client) {
4455
- const client = slot.client;
4525
+ slot.state = "dormant";
4526
+ const client = slot.client;
4527
+ slot.client = void 0;
4528
+ if (client) {
4456
4529
  client.removeExitListener?.(ctx.onChildExit);
4457
4530
  if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
4458
4531
  client.removeToolsChangedListener?.(ctx.onToolsChanged);
@@ -4462,10 +4535,8 @@ async function sleepIdleSlot(ctx, slot) {
4462
4535
  } catch (err) {
4463
4536
  ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
4464
4537
  }
4465
- slot.client = void 0;
4466
4538
  }
4467
4539
  slot.onDisconnect = void 0;
4468
- slot.state = "dormant";
4469
4540
  slot.operations.sleepCount++;
4470
4541
  ctx.recordOperation(slot, "sleep", "idle-timeout");
4471
4542
  ctx.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
@@ -5230,6 +5301,7 @@ var MCPServer = class {
5230
5301
  logger;
5231
5302
  resources;
5232
5303
  prompts;
5304
+ inFlightRequests = /* @__PURE__ */ new Map();
5233
5305
  constructor(opts) {
5234
5306
  this.host = opts.host;
5235
5307
  this.serverInfo = opts.serverInfo ?? {
@@ -5254,32 +5326,44 @@ var MCPServer = class {
5254
5326
  } catch {
5255
5327
  return this.encodeError(null, PARSE_ERROR, "Parse error");
5256
5328
  }
5257
- if (typeof msg !== "object" || msg === null || typeof msg.method !== "string") {
5329
+ const validId = msg?.id === void 0 || msg.id === null || typeof msg.id === "string" || typeof msg.id === "number";
5330
+ if (typeof msg !== "object" || msg === null || msg.jsonrpc !== "2.0" || typeof msg.method !== "string" || !validId) {
5258
5331
  const id = msg && typeof msg === "object" ? msg.id ?? null : null;
5259
5332
  return this.encodeError(id ?? null, INVALID_REQUEST, "Invalid Request");
5260
5333
  }
5261
5334
  const isNotification = msg.id === void 0 || msg.id === null;
5262
5335
  if (isNotification) {
5336
+ if (msg.method === "notifications/cancelled") {
5337
+ const input = paramsRecord(msg.params);
5338
+ const requestId2 = input["requestId"];
5339
+ if (typeof requestId2 === "number" || typeof requestId2 === "string") {
5340
+ const reason = typeof input["reason"] === "string" && input["reason"].trim().length > 0 ? input["reason"].trim().slice(0, 500) : "MCP request cancelled by client";
5341
+ this.inFlightRequests.get(requestId2)?.abort(new Error(reason));
5342
+ }
5343
+ }
5263
5344
  return null;
5264
5345
  }
5346
+ const requestId = expectDefined3(msg.id);
5347
+ const controller = new AbortController();
5348
+ this.inFlightRequests.set(requestId, controller);
5265
5349
  try {
5266
- const result = await this.dispatch(msg.method, msg.params);
5350
+ const result = await this.dispatch(msg.method, msg.params, controller.signal);
5267
5351
  if (result === METHOD_NOT_FOUND_SENTINEL) {
5268
- return this.encodeError(
5269
- expectDefined3(msg.id),
5270
- METHOD_NOT_FOUND,
5271
- `Method not found: ${msg.method}`
5272
- );
5352
+ return this.encodeError(requestId, METHOD_NOT_FOUND, `Method not found: ${msg.method}`);
5273
5353
  }
5274
5354
  return JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
5275
5355
  } catch (err) {
5276
5356
  const message = toErrorMessage2(err);
5277
5357
  this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
5278
5358
  const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
5279
- return this.encodeError(expectDefined3(msg.id), code, message);
5359
+ return this.encodeError(requestId, code, message);
5360
+ } finally {
5361
+ if (this.inFlightRequests.get(requestId) === controller) {
5362
+ this.inFlightRequests.delete(requestId);
5363
+ }
5280
5364
  }
5281
5365
  }
5282
- async dispatch(method, params) {
5366
+ async dispatch(method, params, signal) {
5283
5367
  switch (method) {
5284
5368
  case "initialize":
5285
5369
  return {
@@ -5306,7 +5390,7 @@ var MCPServer = class {
5306
5390
  }
5307
5391
  const args = p.arguments && typeof p.arguments === "object" && !Array.isArray(p.arguments) ? p.arguments : {};
5308
5392
  await this.assertArgumentsMatchSchema(p.name, args);
5309
- const res = await this.host.callTool(p.name, args);
5393
+ const res = await this.host.callTool(p.name, args, { signal });
5310
5394
  return { content: toContentBlocks(res.content), isError: res.isError };
5311
5395
  }
5312
5396
  case "resources/list": {
@@ -5463,16 +5547,22 @@ function renderPromptTemplate(template, args) {
5463
5547
  });
5464
5548
  }
5465
5549
  var METHOD_NOT_FOUND_SENTINEL = /* @__PURE__ */ Symbol("method-not-found");
5550
+ function isMCPContentBlock(c) {
5551
+ if (!c || typeof c !== "object") return false;
5552
+ const type = c["type"];
5553
+ return type === "text" || type === "image" || type === "audio" || type === "resource" || type === "resource_link";
5554
+ }
5466
5555
  function toContentBlocks(content) {
5467
5556
  if (typeof content === "string") return [{ type: "text", text: content }];
5468
5557
  if (Array.isArray(content)) {
5469
- const allBlocks = content.every(
5470
- (c) => c && typeof c === "object" && c.type === "text"
5471
- );
5558
+ const allBlocks = content.every(isMCPContentBlock);
5472
5559
  if (allBlocks) return content;
5473
5560
  return [{ type: "text", text: content.map((c) => stringifyItem(c)).join("\n") }];
5474
5561
  }
5475
5562
  if (content === void 0 || content === null) return [{ type: "text", text: "" }];
5563
+ if (isMCPContentBlock(content)) {
5564
+ return [content];
5565
+ }
5476
5566
  return [{ type: "text", text: stringifyItem(content) }];
5477
5567
  }
5478
5568
  function stringifyItem(c) {
package/dist/manage.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface McpServerInput {
14
14
  env?: Record<string, string> | undefined;
15
15
  url?: string | undefined;
16
16
  headers?: Record<string, string> | undefined;
17
+ bearerTokenEnv?: string | undefined;
17
18
  allowedTools?: string[] | undefined;
18
19
  permission?: Permission | undefined;
19
20
  /** Lazy connect — spawn the process only on first tool call (see config). */
@@ -15,6 +15,7 @@ export declare function manifestConfigHash(cfg: {
15
15
  url?: string | undefined;
16
16
  env?: Record<string, string> | undefined;
17
17
  headers?: Record<string, string> | undefined;
18
+ bearerTokenEnv?: string | undefined;
18
19
  passthroughEnv?: string[] | undefined;
19
20
  }): string;
20
21
  /**
package/dist/server.d.ts CHANGED
@@ -29,7 +29,9 @@ export interface MCPServerCallResult {
29
29
  */
30
30
  export interface MCPServerToolHost {
31
31
  listTools(): MCPServerTool[] | Promise<MCPServerTool[]>;
32
- callTool(name: string, args: Record<string, unknown>): Promise<MCPServerCallResult>;
32
+ callTool(name: string, args: Record<string, unknown>, opts?: {
33
+ signal?: AbortSignal | undefined;
34
+ }): Promise<MCPServerCallResult>;
33
35
  }
34
36
  export interface MCPServerResource {
35
37
  uri: string;
@@ -72,6 +74,7 @@ export declare class MCPServer {
72
74
  private readonly logger?;
73
75
  private readonly resources;
74
76
  private readonly prompts;
77
+ private readonly inFlightRequests;
75
78
  constructor(opts: MCPServerOptions);
76
79
  /**
77
80
  * Handle one raw JSON-RPC line. Returns the response JSON string for
@@ -121,8 +124,8 @@ export declare class InvalidToolArgumentsError extends Error {
121
124
  }
122
125
  /** Normalize a host result's content into MCP content blocks. */
123
126
  export declare function toContentBlocks(content: unknown): Array<{
124
- type: 'text';
125
- text: string;
127
+ type: string;
128
+ [key: string]: unknown;
126
129
  }>;
127
130
  export interface ServeStdioHandle {
128
131
  /** Stop reading and detach listeners. Does not exit the process. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/mcp",
3
- "version": "1.0.17",
3
+ "version": "1.0.24",
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.17",
29
+ "@wrongstack/core": "1.0.24",
30
30
  "undici": "^8.10.2"
31
31
  },
32
32
  "devDependencies": {