@wrongstack/mcp 1.0.19 → 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;
@@ -1361,19 +1462,9 @@ function validateTransportUrl(rawUrl) {
1361
1462
  const hostname = url.hostname;
1362
1463
  const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1363
1464
  const ipVersion = net2.isIP(host);
1364
- if (ipVersion === 4) {
1365
- const parts = host.split(".").map(Number);
1366
- if (parts[0] === 169 && parts[1] === 254) {
1367
- throw new ConfigError({
1368
- message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1369
- code: "CONFIG_INVALID",
1370
- context: { field: "url", rawUrl, hostname }
1371
- });
1372
- }
1373
- } else if (ipVersion === 6) {
1374
- const lower = host.toLowerCase();
1375
- const linkLocal = /^fe[89ab]/.test(lower);
1376
- if (linkLocal || lower === "fd00:ec2::254") {
1465
+ if (ipVersion !== 0) {
1466
+ const classification = classifyTransportAddress(host, ipVersion);
1467
+ if (classification === "blocked") {
1377
1468
  throw new ConfigError({
1378
1469
  message: `MCP transport: blocked link-local/IMDS address "${hostname}" \u2014 likely not a valid MCP server`,
1379
1470
  code: "CONFIG_INVALID",
@@ -1382,7 +1473,7 @@ function validateTransportUrl(rawUrl) {
1382
1473
  }
1383
1474
  }
1384
1475
  if (url.protocol === "http:") {
1385
- const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1476
+ const isLoopback = host === "localhost" || ipVersion !== 0 && classifyTransportAddress(host, ipVersion) === "loopback";
1386
1477
  if (!isLoopback) {
1387
1478
  throw new ConfigError({
1388
1479
  message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname}"`,
@@ -2535,6 +2626,10 @@ var MCPClient = class _MCPClient {
2535
2626
  this.opts = opts;
2536
2627
  }
2537
2628
  opts;
2629
+ capabilityClient = new MCPCapabilityClient(
2630
+ this.requestCapability.bind(this),
2631
+ this.requireResourceSubscriptions.bind(this)
2632
+ );
2538
2633
  /**
2539
2634
  * Maximum bytes the rx buffer may accumulate before the connection is
2540
2635
  * forcefully closed. A well-behaved JSON-RPC server emits newline-delimited
@@ -2760,7 +2855,7 @@ var MCPClient = class _MCPClient {
2760
2855
  const httpOpts = {
2761
2856
  name: this.opts.name,
2762
2857
  url: this.opts.url,
2763
- headers: this.opts.headers,
2858
+ headers: resolveHttpBearerHeaders(this.opts),
2764
2859
  startupTimeoutMs: this.opts.startupTimeoutMs,
2765
2860
  requestTimeoutMs: this.opts.requestTimeoutMs,
2766
2861
  authorizationProvider: this.opts.authorizationProvider,
@@ -2811,7 +2906,7 @@ var MCPClient = class _MCPClient {
2811
2906
  const httpOpts = {
2812
2907
  name: this.opts.name,
2813
2908
  url: this.opts.url,
2814
- headers: this.opts.headers,
2909
+ headers: resolveHttpBearerHeaders(this.opts),
2815
2910
  startupTimeoutMs: this.opts.startupTimeoutMs,
2816
2911
  requestTimeoutMs: this.opts.requestTimeoutMs,
2817
2912
  authorizationProvider: this.opts.authorizationProvider,
@@ -2875,77 +2970,25 @@ var MCPClient = class _MCPClient {
2875
2970
  };
2876
2971
  }
2877
2972
  async listResources(opts = {}) {
2878
- const params = pageParams(opts.cursor, "resources/list cursor");
2879
- return this.requestCapability(
2880
- "resources",
2881
- "resources/list",
2882
- params,
2883
- parseListResourcesResult,
2884
- opts
2885
- );
2973
+ return this.capabilityClient.listResources(opts);
2886
2974
  }
2887
2975
  async listResourceTemplates(opts = {}) {
2888
- const params = pageParams(opts.cursor, "resources/templates/list cursor");
2889
- return this.requestCapability(
2890
- "resources",
2891
- "resources/templates/list",
2892
- params,
2893
- parseListResourceTemplatesResult,
2894
- opts
2895
- );
2976
+ return this.capabilityClient.listResourceTemplates(opts);
2896
2977
  }
2897
2978
  async readResource(uri, opts = {}) {
2898
- validateProtocolString(uri, "resource URI");
2899
- return this.requestCapability(
2900
- "resources",
2901
- "resources/read",
2902
- { uri },
2903
- parseReadResourceResult,
2904
- opts
2905
- );
2979
+ return this.capabilityClient.readResource(uri, opts);
2906
2980
  }
2907
2981
  async subscribeResource(uri, opts = {}) {
2908
- validateProtocolString(uri, "resource URI");
2909
- this.requireResourceSubscriptions("resources/subscribe");
2910
- await this.requestCapability(
2911
- "resources",
2912
- "resources/subscribe",
2913
- { uri },
2914
- parseEmptyResult,
2915
- opts
2916
- );
2982
+ return this.capabilityClient.subscribeResource(uri, opts);
2917
2983
  }
2918
2984
  async unsubscribeResource(uri, opts = {}) {
2919
- validateProtocolString(uri, "resource URI");
2920
- this.requireResourceSubscriptions("resources/unsubscribe");
2921
- await this.requestCapability(
2922
- "resources",
2923
- "resources/unsubscribe",
2924
- { uri },
2925
- parseEmptyResult,
2926
- opts
2927
- );
2985
+ return this.capabilityClient.unsubscribeResource(uri, opts);
2928
2986
  }
2929
2987
  async listPrompts(opts = {}) {
2930
- const params = pageParams(opts.cursor, "prompts/list cursor");
2931
- return this.requestCapability("prompts", "prompts/list", params, parseListPromptsResult, opts);
2988
+ return this.capabilityClient.listPrompts(opts);
2932
2989
  }
2933
2990
  async getPrompt(name, args, opts = {}) {
2934
- validateProtocolString(name, "prompt name");
2935
- if (args && Object.keys(args).length > 64) {
2936
- throw new Error("MCP prompt arguments exceed the limit of 64");
2937
- }
2938
- for (const [key, value] of Object.entries(args ?? {})) {
2939
- validateProtocolString(key, "prompt argument name");
2940
- validateProtocolString(value, `prompt argument "${key}"`, true);
2941
- }
2942
- return this.requestCapability(
2943
- "prompts",
2944
- "prompts/get",
2945
- args === void 0 ? { name } : { name, arguments: args },
2946
- parseGetPromptResult,
2947
- opts
2948
- );
2991
+ return this.capabilityClient.getPrompt(name, args, opts);
2949
2992
  }
2950
2993
  async close() {
2951
2994
  if (this.closePromise) return this.closePromise;
@@ -3433,11 +3476,25 @@ function requireIdentity(value, label) {
3433
3476
  import { randomBytes as randomBytes3 } from "node:crypto";
3434
3477
  import * as fs from "node:fs/promises";
3435
3478
  async function readConfig(path2) {
3479
+ let raw;
3436
3480
  try {
3437
- 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);
3438
3489
  } catch {
3439
- 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`);
3440
3496
  }
3497
+ return parsed;
3441
3498
  }
3442
3499
  async function writeConfig(path2, cfg) {
3443
3500
  const raw = JSON.stringify(cfg, null, 2);
@@ -3509,6 +3566,8 @@ function buildConfig(input, base) {
3509
3566
  if (url !== void 0) cfg.url = url;
3510
3567
  const headers = input.headers ?? base?.headers;
3511
3568
  if (headers !== void 0) cfg.headers = headers;
3569
+ const bearerTokenEnv = input.bearerTokenEnv ?? base?.bearerTokenEnv;
3570
+ if (bearerTokenEnv !== void 0) cfg.bearerTokenEnv = bearerTokenEnv;
3512
3571
  const allowedTools = input.allowedTools ?? base?.allowedTools;
3513
3572
  if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
3514
3573
  const permission = input.permission ?? base?.permission;
@@ -3753,6 +3812,7 @@ function manifestConfigHash(cfg) {
3753
3812
  url: cfg.url ?? null,
3754
3813
  env: sortedEntries(cfg.env),
3755
3814
  headers: sortedEntries(cfg.headers),
3815
+ bearerTokenEnv: cfg.bearerTokenEnv ?? null,
3756
3816
  passthroughEnv: passthroughEntries(cfg.passthroughEnv)
3757
3817
  });
3758
3818
  return createHash2("sha256").update(basis).digest("hex").slice(0, 16);
@@ -4070,7 +4130,7 @@ function renderContentBlock(item) {
4070
4130
  if (type === "image" || type === "audio") {
4071
4131
  const mime = typeof item["mimeType"] === "string" ? item["mimeType"] : "unknown type";
4072
4132
  const data = typeof item["data"] === "string" ? item["data"] : "";
4073
- 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));
4074
4134
  return `[${type} content: ${mime}, ~${kb} KB \u2014 binary payload not inlined]`;
4075
4135
  }
4076
4136
  if (type === "resource" && item["resource"] && typeof item["resource"] === "object") {
@@ -4087,6 +4147,9 @@ ${resource["text"]}`;
4087
4147
  if (type === "resource_link" && typeof item["uri"] === "string") {
4088
4148
  return `[resource link: ${item["uri"]}]`;
4089
4149
  }
4150
+ if ("text" in item) {
4151
+ return String(item["text"] ?? "");
4152
+ }
4090
4153
  return JSON.stringify(item);
4091
4154
  }
4092
4155
  function stringify(c) {
@@ -4100,10 +4163,7 @@ function stringify(c) {
4100
4163
  }).join("\n");
4101
4164
  }
4102
4165
  if (c && typeof c === "object") {
4103
- if ("text" in c) {
4104
- return String(c.text);
4105
- }
4106
- return JSON.stringify(c);
4166
+ return renderContentBlock(c);
4107
4167
  }
4108
4168
  return String(c ?? "");
4109
4169
  }
@@ -4250,6 +4310,7 @@ async function attemptConnectSlot(ctx, slot) {
4250
4310
  env: slot.cfg.env,
4251
4311
  url: slot.cfg.url,
4252
4312
  headers: slot.cfg.headers,
4313
+ bearerTokenEnv: slot.cfg.bearerTokenEnv,
4253
4314
  startupTimeoutMs: slot.cfg.startupTimeoutMs,
4254
4315
  requestTimeoutMs: slot.cfg.requestTimeoutMs,
4255
4316
  cwd: ctx.cwd,
@@ -4461,8 +4522,10 @@ async function sleepIdleSlot(ctx, slot) {
4461
4522
  clearTimeout(slot.reconnectTimer);
4462
4523
  slot.reconnectTimer = void 0;
4463
4524
  }
4464
- if (slot.client) {
4465
- const client = slot.client;
4525
+ slot.state = "dormant";
4526
+ const client = slot.client;
4527
+ slot.client = void 0;
4528
+ if (client) {
4466
4529
  client.removeExitListener?.(ctx.onChildExit);
4467
4530
  if (slot.onDisconnect) client.removeDisconnectListener?.(slot.onDisconnect);
4468
4531
  client.removeToolsChangedListener?.(ctx.onToolsChanged);
@@ -4472,10 +4535,8 @@ async function sleepIdleSlot(ctx, slot) {
4472
4535
  } catch (err) {
4473
4536
  ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
4474
4537
  }
4475
- slot.client = void 0;
4476
4538
  }
4477
4539
  slot.onDisconnect = void 0;
4478
- slot.state = "dormant";
4479
4540
  slot.operations.sleepCount++;
4480
4541
  ctx.recordOperation(slot, "sleep", "idle-timeout");
4481
4542
  ctx.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
@@ -5240,6 +5301,7 @@ var MCPServer = class {
5240
5301
  logger;
5241
5302
  resources;
5242
5303
  prompts;
5304
+ inFlightRequests = /* @__PURE__ */ new Map();
5243
5305
  constructor(opts) {
5244
5306
  this.host = opts.host;
5245
5307
  this.serverInfo = opts.serverInfo ?? {
@@ -5264,32 +5326,44 @@ var MCPServer = class {
5264
5326
  } catch {
5265
5327
  return this.encodeError(null, PARSE_ERROR, "Parse error");
5266
5328
  }
5267
- 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) {
5268
5331
  const id = msg && typeof msg === "object" ? msg.id ?? null : null;
5269
5332
  return this.encodeError(id ?? null, INVALID_REQUEST, "Invalid Request");
5270
5333
  }
5271
5334
  const isNotification = msg.id === void 0 || msg.id === null;
5272
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
+ }
5273
5344
  return null;
5274
5345
  }
5346
+ const requestId = expectDefined3(msg.id);
5347
+ const controller = new AbortController();
5348
+ this.inFlightRequests.set(requestId, controller);
5275
5349
  try {
5276
- const result = await this.dispatch(msg.method, msg.params);
5350
+ const result = await this.dispatch(msg.method, msg.params, controller.signal);
5277
5351
  if (result === METHOD_NOT_FOUND_SENTINEL) {
5278
- return this.encodeError(
5279
- expectDefined3(msg.id),
5280
- METHOD_NOT_FOUND,
5281
- `Method not found: ${msg.method}`
5282
- );
5352
+ return this.encodeError(requestId, METHOD_NOT_FOUND, `Method not found: ${msg.method}`);
5283
5353
  }
5284
5354
  return JSON.stringify({ jsonrpc: "2.0", id: msg.id, result });
5285
5355
  } catch (err) {
5286
5356
  const message = toErrorMessage2(err);
5287
5357
  this.logger?.warn?.(`MCP server: method "${msg.method}" threw: ${message}`);
5288
5358
  const code = err instanceof InvalidToolArgumentsError ? INVALID_PARAMS : INTERNAL_ERROR;
5289
- 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
+ }
5290
5364
  }
5291
5365
  }
5292
- async dispatch(method, params) {
5366
+ async dispatch(method, params, signal) {
5293
5367
  switch (method) {
5294
5368
  case "initialize":
5295
5369
  return {
@@ -5316,7 +5390,7 @@ var MCPServer = class {
5316
5390
  }
5317
5391
  const args = p.arguments && typeof p.arguments === "object" && !Array.isArray(p.arguments) ? p.arguments : {};
5318
5392
  await this.assertArgumentsMatchSchema(p.name, args);
5319
- const res = await this.host.callTool(p.name, args);
5393
+ const res = await this.host.callTool(p.name, args, { signal });
5320
5394
  return { content: toContentBlocks(res.content), isError: res.isError };
5321
5395
  }
5322
5396
  case "resources/list": {
@@ -5473,16 +5547,22 @@ function renderPromptTemplate(template, args) {
5473
5547
  });
5474
5548
  }
5475
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
+ }
5476
5555
  function toContentBlocks(content) {
5477
5556
  if (typeof content === "string") return [{ type: "text", text: content }];
5478
5557
  if (Array.isArray(content)) {
5479
- const allBlocks = content.every(
5480
- (c) => c && typeof c === "object" && c.type === "text"
5481
- );
5558
+ const allBlocks = content.every(isMCPContentBlock);
5482
5559
  if (allBlocks) return content;
5483
5560
  return [{ type: "text", text: content.map((c) => stringifyItem(c)).join("\n") }];
5484
5561
  }
5485
5562
  if (content === void 0 || content === null) return [{ type: "text", text: "" }];
5563
+ if (isMCPContentBlock(content)) {
5564
+ return [content];
5565
+ }
5486
5566
  return [{ type: "text", text: stringifyItem(content) }];
5487
5567
  }
5488
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.19",
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.19",
29
+ "@wrongstack/core": "1.0.24",
30
30
  "undici": "^8.10.2"
31
31
  },
32
32
  "devDependencies": {