@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.
- package/dist/client-capabilities.d.ts +15 -0
- package/dist/client-process.d.ts +6 -0
- package/dist/client-types.d.ts +62 -0
- package/dist/client.d.ts +4 -47
- package/dist/index.js +283 -193
- package/dist/manage.d.ts +1 -0
- package/dist/manifest-cache.d.ts +1 -0
- package/dist/server.d.ts +6 -3
- package/package.json +2 -2
|
@@ -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
|
package/dist/client-process.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
1366
|
-
const
|
|
1367
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4455
|
-
|
|
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
|
-
|
|
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(
|
|
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). */
|
package/dist/manifest-cache.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
125
|
-
|
|
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.
|
|
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.
|
|
29
|
+
"@wrongstack/core": "1.0.24",
|
|
30
30
|
"undici": "^8.10.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|