toolcraft 0.0.5 → 0.0.7
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/README.md +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +77 -59
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
- package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
- package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
- package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
- package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
- package/node_modules/@poe-code/agent-defs/package.json +20 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
- package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
- package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
- package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
- package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
- package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
- package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
- package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
- package/node_modules/@poe-code/config-mutations/package.json +33 -0
- package/node_modules/@poe-code/file-lock/README.md +52 -0
- package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
- package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
- package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
- package/node_modules/@poe-code/file-lock/package.json +23 -0
- package/node_modules/auth-store/README.md +47 -0
- package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
- package/node_modules/auth-store/dist/create-secret-store.js +35 -0
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
- package/node_modules/auth-store/dist/index.d.ts +7 -0
- package/node_modules/auth-store/dist/index.js +4 -0
- package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
- package/node_modules/auth-store/dist/keychain-store.js +111 -0
- package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
- package/node_modules/auth-store/dist/provider-store.js +28 -0
- package/node_modules/auth-store/dist/types.d.ts +20 -0
- package/node_modules/auth-store/dist/types.js +1 -0
- package/node_modules/auth-store/package.json +25 -0
- package/node_modules/mcp-oauth/README.md +31 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
- package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
- package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
- package/node_modules/mcp-oauth/dist/client/types.js +1 -0
- package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
- package/node_modules/mcp-oauth/dist/index.js +7 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
- package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
- package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
- package/node_modules/mcp-oauth/package.json +31 -0
- package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/index.js +1 -0
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
- package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
- package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
- package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
- package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
- package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
- package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
- package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
- package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
- package/node_modules/tiny-mcp-client/package.json +22 -0
- package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
- package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
- package/node_modules/tiny-mcp-client/src/index.ts +94 -0
- package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
- package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
- package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
- package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
- package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
- package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
- package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
- package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
- package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
- package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
- package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
- package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
- package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
- package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
- package/package.json +24 -11
|
@@ -0,0 +1,2404 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { createOAuthClientProvider, OAuthError, } from "mcp-oauth";
|
|
4
|
+
import { OAuthMetadataDiscovery, parseBearerWwwAuthenticateHeader, } from "./oauth-discovery.js";
|
|
5
|
+
export { OAuthMetadataDiscovery, discoverOAuthMetadata, parseBearerWwwAuthenticateHeader, resolveAuthorizationServerMetadataUrl, resolveProtectedResourceMetadataUrl, } from "./oauth-discovery.js";
|
|
6
|
+
export { createAuthStoreSessionStore, createDefaultOAuthClientProvider, } from "mcp-oauth";
|
|
7
|
+
const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
8
|
+
export class McpClient {
|
|
9
|
+
currentState = "disconnected";
|
|
10
|
+
currentServerCapabilities = null;
|
|
11
|
+
currentServerInfo = null;
|
|
12
|
+
currentInstructions;
|
|
13
|
+
options;
|
|
14
|
+
transport = null;
|
|
15
|
+
messageLayer = null;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
get state() {
|
|
20
|
+
return this.currentState;
|
|
21
|
+
}
|
|
22
|
+
get serverCapabilities() {
|
|
23
|
+
return this.currentServerCapabilities;
|
|
24
|
+
}
|
|
25
|
+
get serverInfo() {
|
|
26
|
+
return this.currentServerInfo;
|
|
27
|
+
}
|
|
28
|
+
get instructions() {
|
|
29
|
+
return this.currentInstructions;
|
|
30
|
+
}
|
|
31
|
+
getMessageLayerOrThrow() {
|
|
32
|
+
if (this.currentState === "disconnected") {
|
|
33
|
+
throw new Error("MCP client is disconnected");
|
|
34
|
+
}
|
|
35
|
+
if (this.currentState === "closed") {
|
|
36
|
+
throw new Error("MCP client is closed");
|
|
37
|
+
}
|
|
38
|
+
if (this.messageLayer === null) {
|
|
39
|
+
throw new Error("MCP client is disconnected");
|
|
40
|
+
}
|
|
41
|
+
return this.messageLayer;
|
|
42
|
+
}
|
|
43
|
+
async connect(transport) {
|
|
44
|
+
if (this.currentState !== "disconnected" && this.currentState !== "closed") {
|
|
45
|
+
throw new Error("MCP client is already connected");
|
|
46
|
+
}
|
|
47
|
+
const transportClosedReason = transport.closed
|
|
48
|
+
.then((closedEvent) => closedEvent.reason)
|
|
49
|
+
.catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
50
|
+
const messageLayer = new JsonRpcMessageLayer(transport.readable, transport.writable, 30_000, transportClosedReason);
|
|
51
|
+
const { onSamplingRequest, onRootsList, onToolsChanged, onResourcesChanged, onResourceUpdated, onPromptsChanged, onLog, onProgress, } = this.options;
|
|
52
|
+
messageLayer.onRequest("ping", () => ({}));
|
|
53
|
+
if (onSamplingRequest !== undefined) {
|
|
54
|
+
messageLayer.onRequest("sampling/createMessage", (params) => onSamplingRequest(params));
|
|
55
|
+
}
|
|
56
|
+
if (onRootsList !== undefined) {
|
|
57
|
+
messageLayer.onRequest("roots/list", async () => ({
|
|
58
|
+
roots: await onRootsList(),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
messageLayer.onNotification("notifications/tools/list_changed", async () => {
|
|
62
|
+
if (onToolsChanged === undefined) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await onToolsChanged();
|
|
66
|
+
});
|
|
67
|
+
messageLayer.onNotification("notifications/resources/list_changed", async () => {
|
|
68
|
+
if (onResourcesChanged === undefined) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await onResourcesChanged();
|
|
72
|
+
});
|
|
73
|
+
messageLayer.onNotification("notifications/resources/updated", async (params) => {
|
|
74
|
+
if (onResourceUpdated === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const { uri } = params;
|
|
81
|
+
if (typeof uri !== "string") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await onResourceUpdated(uri);
|
|
85
|
+
});
|
|
86
|
+
messageLayer.onNotification("notifications/prompts/list_changed", async () => {
|
|
87
|
+
if (onPromptsChanged === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await onPromptsChanged();
|
|
91
|
+
});
|
|
92
|
+
messageLayer.onNotification("notifications/message", async (params) => {
|
|
93
|
+
if (onLog === undefined || !isObjectRecord(params) || !isLogLevel(params.level)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!hasOwn(params, "data")) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const message = {
|
|
100
|
+
level: params.level,
|
|
101
|
+
data: params.data,
|
|
102
|
+
};
|
|
103
|
+
if (params.logger !== undefined) {
|
|
104
|
+
if (typeof params.logger !== "string") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
message.logger = params.logger;
|
|
108
|
+
}
|
|
109
|
+
await onLog(message);
|
|
110
|
+
});
|
|
111
|
+
messageLayer.onNotification("notifications/progress", async (params) => {
|
|
112
|
+
if (onProgress === undefined || !isObjectRecord(params)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { progressToken, progress } = params;
|
|
116
|
+
if (!isRequestId(progressToken) || typeof progress !== "number") {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const progressParams = {
|
|
120
|
+
progressToken,
|
|
121
|
+
progress,
|
|
122
|
+
};
|
|
123
|
+
if (params.total !== undefined) {
|
|
124
|
+
if (typeof params.total !== "number") {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
progressParams.total = params.total;
|
|
128
|
+
}
|
|
129
|
+
if (params.message !== undefined) {
|
|
130
|
+
if (typeof params.message !== "string") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
progressParams.message = params.message;
|
|
134
|
+
}
|
|
135
|
+
await onProgress(progressParams);
|
|
136
|
+
});
|
|
137
|
+
messageLayer.onNotification("notifications/cancelled", () => undefined);
|
|
138
|
+
this.transport = transport;
|
|
139
|
+
this.messageLayer = messageLayer;
|
|
140
|
+
this.currentState = "initializing";
|
|
141
|
+
transport.closed
|
|
142
|
+
.then((closedEvent) => {
|
|
143
|
+
if (this.transport !== transport) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.messageLayer?.dispose(closedEvent.reason);
|
|
147
|
+
this.messageLayer = null;
|
|
148
|
+
this.transport = null;
|
|
149
|
+
this.currentState = "closed";
|
|
150
|
+
})
|
|
151
|
+
.catch((error) => {
|
|
152
|
+
if (this.transport !== transport) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const reason = error instanceof Error ? error : new Error(String(error));
|
|
156
|
+
this.messageLayer?.dispose(reason);
|
|
157
|
+
this.messageLayer = null;
|
|
158
|
+
this.transport = null;
|
|
159
|
+
this.currentState = "closed";
|
|
160
|
+
});
|
|
161
|
+
const capabilities = {
|
|
162
|
+
...(this.options.capabilities ?? {}),
|
|
163
|
+
};
|
|
164
|
+
if (onSamplingRequest !== undefined && capabilities.sampling === undefined) {
|
|
165
|
+
capabilities.sampling = {};
|
|
166
|
+
}
|
|
167
|
+
if (onRootsList !== undefined) {
|
|
168
|
+
capabilities.roots = {
|
|
169
|
+
...(capabilities.roots ?? {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const initializeResult = (await messageLayer.sendRequest("initialize", {
|
|
173
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
174
|
+
clientInfo: this.options.clientInfo,
|
|
175
|
+
capabilities,
|
|
176
|
+
}));
|
|
177
|
+
if (initializeResult.protocolVersion !== MCP_PROTOCOL_VERSION) {
|
|
178
|
+
throw new McpError(ERROR_INVALID_REQUEST, `Unsupported protocol version: ${initializeResult.protocolVersion}`);
|
|
179
|
+
}
|
|
180
|
+
this.currentServerCapabilities = initializeResult.capabilities;
|
|
181
|
+
this.currentServerInfo = initializeResult.serverInfo;
|
|
182
|
+
this.currentInstructions = initializeResult.instructions;
|
|
183
|
+
messageLayer.sendNotification("notifications/initialized");
|
|
184
|
+
this.currentState = "ready";
|
|
185
|
+
return initializeResult;
|
|
186
|
+
}
|
|
187
|
+
getServerCapabilitiesOrThrow() {
|
|
188
|
+
if (this.currentServerCapabilities === null) {
|
|
189
|
+
throw new Error("MCP client has not completed initialization");
|
|
190
|
+
}
|
|
191
|
+
return this.currentServerCapabilities;
|
|
192
|
+
}
|
|
193
|
+
async listTools(params = {}) {
|
|
194
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
195
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
196
|
+
if (serverCapabilities.tools === undefined) {
|
|
197
|
+
throw new Error("Server does not support tools");
|
|
198
|
+
}
|
|
199
|
+
const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
|
|
200
|
+
return (await messageLayer.sendRequest("tools/list", requestParams));
|
|
201
|
+
}
|
|
202
|
+
async callTool(params, options = {}) {
|
|
203
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
204
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
205
|
+
if (serverCapabilities.tools === undefined) {
|
|
206
|
+
throw new Error("Server does not support tools");
|
|
207
|
+
}
|
|
208
|
+
const requestParams = options.progressToken === undefined
|
|
209
|
+
? params
|
|
210
|
+
: {
|
|
211
|
+
...params,
|
|
212
|
+
_meta: {
|
|
213
|
+
progressToken: options.progressToken,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
let requestId;
|
|
217
|
+
const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
|
|
218
|
+
onRequestId: (nextRequestId) => {
|
|
219
|
+
requestId = nextRequestId;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
if (options.signal === undefined) {
|
|
223
|
+
return await requestPromise;
|
|
224
|
+
}
|
|
225
|
+
const signal = options.signal;
|
|
226
|
+
let abortListener;
|
|
227
|
+
const abortPromise = new Promise((_, reject) => {
|
|
228
|
+
const rejectWithAbortReason = () => {
|
|
229
|
+
if (requestId !== undefined) {
|
|
230
|
+
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
231
|
+
}
|
|
232
|
+
reject(signal.reason);
|
|
233
|
+
};
|
|
234
|
+
abortListener = rejectWithAbortReason;
|
|
235
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
236
|
+
if (signal.aborted) {
|
|
237
|
+
signal.removeEventListener("abort", abortListener);
|
|
238
|
+
rejectWithAbortReason();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
return (await Promise.race([requestPromise, abortPromise]));
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
if (abortListener !== undefined) {
|
|
246
|
+
signal.removeEventListener("abort", abortListener);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async listResources(params = {}) {
|
|
251
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
252
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
253
|
+
if (serverCapabilities.resources === undefined) {
|
|
254
|
+
throw new Error("Server does not support resources");
|
|
255
|
+
}
|
|
256
|
+
const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
|
|
257
|
+
return (await messageLayer.sendRequest("resources/list", requestParams));
|
|
258
|
+
}
|
|
259
|
+
async listResourceTemplates(params = {}) {
|
|
260
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
261
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
262
|
+
if (serverCapabilities.resources === undefined) {
|
|
263
|
+
throw new Error("Server does not support resources");
|
|
264
|
+
}
|
|
265
|
+
const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
|
|
266
|
+
return (await messageLayer.sendRequest("resources/templates/list", requestParams));
|
|
267
|
+
}
|
|
268
|
+
async readResource(params) {
|
|
269
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
270
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
271
|
+
if (serverCapabilities.resources === undefined) {
|
|
272
|
+
throw new Error("Server does not support resources");
|
|
273
|
+
}
|
|
274
|
+
return (await messageLayer.sendRequest("resources/read", params));
|
|
275
|
+
}
|
|
276
|
+
async subscribe(uri) {
|
|
277
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
278
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
279
|
+
if (serverCapabilities.resources?.subscribe !== true) {
|
|
280
|
+
throw new Error("Server does not support resource subscriptions");
|
|
281
|
+
}
|
|
282
|
+
await messageLayer.sendRequest("resources/subscribe", { uri });
|
|
283
|
+
}
|
|
284
|
+
async unsubscribe(uri) {
|
|
285
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
286
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
287
|
+
if (serverCapabilities.resources?.subscribe !== true) {
|
|
288
|
+
throw new Error("Server does not support resource subscriptions");
|
|
289
|
+
}
|
|
290
|
+
await messageLayer.sendRequest("resources/unsubscribe", { uri });
|
|
291
|
+
}
|
|
292
|
+
async listPrompts(params = {}) {
|
|
293
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
294
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
295
|
+
if (serverCapabilities.prompts === undefined) {
|
|
296
|
+
throw new Error("Server does not support prompts");
|
|
297
|
+
}
|
|
298
|
+
const requestParams = params.cursor === undefined ? undefined : { cursor: params.cursor };
|
|
299
|
+
return (await messageLayer.sendRequest("prompts/list", requestParams));
|
|
300
|
+
}
|
|
301
|
+
async getPrompt(params) {
|
|
302
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
303
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
304
|
+
if (serverCapabilities.prompts === undefined) {
|
|
305
|
+
throw new Error("Server does not support prompts");
|
|
306
|
+
}
|
|
307
|
+
return (await messageLayer.sendRequest("prompts/get", params));
|
|
308
|
+
}
|
|
309
|
+
async complete(params) {
|
|
310
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
311
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
312
|
+
if (serverCapabilities.completions === undefined) {
|
|
313
|
+
throw new Error("Server does not support completions");
|
|
314
|
+
}
|
|
315
|
+
return (await messageLayer.sendRequest("completion/complete", params));
|
|
316
|
+
}
|
|
317
|
+
async setLogLevel(level) {
|
|
318
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
319
|
+
const serverCapabilities = this.getServerCapabilitiesOrThrow();
|
|
320
|
+
if (serverCapabilities.logging === undefined) {
|
|
321
|
+
throw new Error("Server does not support logging");
|
|
322
|
+
}
|
|
323
|
+
await messageLayer.sendRequest("logging/setLevel", { level });
|
|
324
|
+
}
|
|
325
|
+
async cancel(requestId, reason) {
|
|
326
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
327
|
+
const params = { requestId };
|
|
328
|
+
if (reason !== undefined) {
|
|
329
|
+
params.reason = reason;
|
|
330
|
+
}
|
|
331
|
+
messageLayer.sendNotification("notifications/cancelled", params);
|
|
332
|
+
}
|
|
333
|
+
async sendRootsChanged() {
|
|
334
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
335
|
+
messageLayer.sendNotification("notifications/roots/list_changed");
|
|
336
|
+
}
|
|
337
|
+
async ping() {
|
|
338
|
+
const messageLayer = this.getMessageLayerOrThrow();
|
|
339
|
+
await messageLayer.sendRequest("ping");
|
|
340
|
+
}
|
|
341
|
+
async close() {
|
|
342
|
+
if (this.currentState === "closed") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const closeError = new Error("MCP client closed");
|
|
346
|
+
this.messageLayer?.dispose(closeError);
|
|
347
|
+
this.transport?.dispose(closeError);
|
|
348
|
+
this.messageLayer = null;
|
|
349
|
+
this.transport = null;
|
|
350
|
+
this.currentState = "closed";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export const ERROR_PARSE = -32700;
|
|
354
|
+
export const ERROR_INVALID_REQUEST = -32600;
|
|
355
|
+
export const ERROR_METHOD_NOT_FOUND = -32601;
|
|
356
|
+
export const ERROR_INVALID_PARAMS = -32602;
|
|
357
|
+
export const ERROR_INTERNAL = -32603;
|
|
358
|
+
export function createInMemoryTransportPair() {
|
|
359
|
+
const clientToServer = new PassThrough();
|
|
360
|
+
const serverToClient = new PassThrough();
|
|
361
|
+
let disposed = false;
|
|
362
|
+
let resolveClosed;
|
|
363
|
+
const closed = new Promise((resolve) => {
|
|
364
|
+
resolveClosed = resolve;
|
|
365
|
+
});
|
|
366
|
+
const resolveClosedOnce = (reason) => {
|
|
367
|
+
if (resolveClosed === undefined) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const currentResolve = resolveClosed;
|
|
371
|
+
resolveClosed = undefined;
|
|
372
|
+
currentResolve({ reason });
|
|
373
|
+
};
|
|
374
|
+
const dispose = (reason = new Error("In-memory transport disposed")) => {
|
|
375
|
+
if (disposed) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
disposed = true;
|
|
379
|
+
if (!clientToServer.destroyed && !clientToServer.writableEnded) {
|
|
380
|
+
clientToServer.end();
|
|
381
|
+
}
|
|
382
|
+
if (!serverToClient.destroyed && !serverToClient.writableEnded) {
|
|
383
|
+
serverToClient.end();
|
|
384
|
+
}
|
|
385
|
+
resolveClosedOnce(reason);
|
|
386
|
+
};
|
|
387
|
+
clientToServer.once("error", (error) => {
|
|
388
|
+
dispose(error instanceof Error ? error : new Error(String(error)));
|
|
389
|
+
});
|
|
390
|
+
serverToClient.once("error", (error) => {
|
|
391
|
+
dispose(error instanceof Error ? error : new Error(String(error)));
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
clientTransport: {
|
|
395
|
+
readable: serverToClient,
|
|
396
|
+
writable: clientToServer,
|
|
397
|
+
closed,
|
|
398
|
+
dispose,
|
|
399
|
+
},
|
|
400
|
+
serverTransport: {
|
|
401
|
+
readable: clientToServer,
|
|
402
|
+
writable: serverToClient,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export class SdkTransportAdapter {
|
|
407
|
+
readable;
|
|
408
|
+
writable;
|
|
409
|
+
closed;
|
|
410
|
+
sdkTransport;
|
|
411
|
+
readStream = new PassThrough();
|
|
412
|
+
writeStream = new PassThrough();
|
|
413
|
+
resolveClosed;
|
|
414
|
+
disposed = false;
|
|
415
|
+
constructor(sdkTransport) {
|
|
416
|
+
this.sdkTransport = sdkTransport;
|
|
417
|
+
this.readable = this.readStream;
|
|
418
|
+
this.writable = this.writeStream;
|
|
419
|
+
this.closed = new Promise((resolve) => {
|
|
420
|
+
this.resolveClosed = resolve;
|
|
421
|
+
});
|
|
422
|
+
this.sdkTransport.onmessage = (message) => {
|
|
423
|
+
this.readStream.write(serializeJsonRpcMessage(message));
|
|
424
|
+
};
|
|
425
|
+
this.sdkTransport.onclose = () => {
|
|
426
|
+
this.dispose(new Error("SDK transport closed"));
|
|
427
|
+
};
|
|
428
|
+
this.sdkTransport.onerror = (error) => {
|
|
429
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
430
|
+
};
|
|
431
|
+
this.readStream.once("error", (error) => {
|
|
432
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
433
|
+
});
|
|
434
|
+
this.writeStream.once("error", (error) => {
|
|
435
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
436
|
+
});
|
|
437
|
+
this.consumeWrittenLines().catch((error) => {
|
|
438
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
439
|
+
});
|
|
440
|
+
this.sdkTransport.start().catch((error) => {
|
|
441
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
dispose(reason = new Error("SDK transport adapter disposed")) {
|
|
445
|
+
if (this.disposed) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.disposed = true;
|
|
449
|
+
if (!this.writeStream.destroyed && !this.writeStream.writableEnded) {
|
|
450
|
+
this.writeStream.end();
|
|
451
|
+
}
|
|
452
|
+
if (!this.readStream.destroyed && !this.readStream.writableEnded) {
|
|
453
|
+
this.readStream.end();
|
|
454
|
+
}
|
|
455
|
+
if (this.resolveClosed !== undefined) {
|
|
456
|
+
const resolveClosed = this.resolveClosed;
|
|
457
|
+
this.resolveClosed = undefined;
|
|
458
|
+
resolveClosed({ reason });
|
|
459
|
+
}
|
|
460
|
+
this.sdkTransport.close().catch(() => undefined);
|
|
461
|
+
}
|
|
462
|
+
async consumeWrittenLines() {
|
|
463
|
+
for await (const line of readLines(this.writeStream)) {
|
|
464
|
+
if (this.disposed || line.length === 0) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
let parsedMessage;
|
|
468
|
+
try {
|
|
469
|
+
parsedMessage = JSON.parse(line);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
throw new Error(`Malformed JSON line: ${line}`);
|
|
473
|
+
}
|
|
474
|
+
if (typeof parsedMessage !== "object" || parsedMessage === null || Array.isArray(parsedMessage)) {
|
|
475
|
+
throw new Error(`Malformed JSON line: ${line}`);
|
|
476
|
+
}
|
|
477
|
+
await this.sdkTransport.send(parsedMessage);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
export async function createSdkTestPair(server, createClient) {
|
|
482
|
+
const { InMemoryTransport } = await import("@modelcontextprotocol/sdk/inMemory.js");
|
|
483
|
+
const [clientSdkTransport, serverSdkTransport] = InMemoryTransport.createLinkedPair();
|
|
484
|
+
const clientTransport = new SdkTransportAdapter(clientSdkTransport);
|
|
485
|
+
const serverPromise = server.connect(serverSdkTransport);
|
|
486
|
+
const client = createClient();
|
|
487
|
+
try {
|
|
488
|
+
await client.connect(clientTransport);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
clientTransport.dispose(new Error("SDK test pair setup failed"));
|
|
492
|
+
await clientSdkTransport.close();
|
|
493
|
+
await serverSdkTransport.close();
|
|
494
|
+
await serverPromise;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
const cleanup = async () => {
|
|
498
|
+
await client.close();
|
|
499
|
+
clientTransport.dispose(new Error("SDK test pair cleanup"));
|
|
500
|
+
await clientSdkTransport.close();
|
|
501
|
+
await serverSdkTransport.close();
|
|
502
|
+
await serverPromise;
|
|
503
|
+
};
|
|
504
|
+
return { client, cleanup };
|
|
505
|
+
}
|
|
506
|
+
export async function createMockEchoToolServer() {
|
|
507
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
508
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
509
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
510
|
+
]);
|
|
511
|
+
const server = new Server({ name: "mock-echo-tool-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
512
|
+
const echoTool = {
|
|
513
|
+
name: "echo",
|
|
514
|
+
description: "Echoes the provided message.",
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: "object",
|
|
517
|
+
properties: {
|
|
518
|
+
message: {
|
|
519
|
+
type: "string",
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
required: ["message"],
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
526
|
+
tools: [echoTool],
|
|
527
|
+
}));
|
|
528
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
529
|
+
if (request.params.name !== "echo") {
|
|
530
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
531
|
+
}
|
|
532
|
+
const message = request.params.arguments?.message;
|
|
533
|
+
if (typeof message !== "string") {
|
|
534
|
+
throw new Error("Echo tool requires a string message argument");
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "text",
|
|
540
|
+
text: message,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
return server;
|
|
546
|
+
}
|
|
547
|
+
export async function createMockMultiToolServer() {
|
|
548
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
549
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
550
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
551
|
+
]);
|
|
552
|
+
const server = new Server({ name: "mock-multi-tool-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
553
|
+
const tools = [
|
|
554
|
+
{
|
|
555
|
+
name: "add",
|
|
556
|
+
description: "Adds two numbers and returns the sum as text.",
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: "object",
|
|
559
|
+
properties: {
|
|
560
|
+
a: { type: "number" },
|
|
561
|
+
b: { type: "number" },
|
|
562
|
+
},
|
|
563
|
+
required: ["a", "b"],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "greet",
|
|
568
|
+
description: "Greets a user with optional formal tone.",
|
|
569
|
+
inputSchema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
name: { type: "string" },
|
|
573
|
+
formal: { type: "boolean" },
|
|
574
|
+
},
|
|
575
|
+
required: ["name"],
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "fail",
|
|
580
|
+
description: "Returns an intentional tool error payload.",
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {},
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
588
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
589
|
+
if (request.params.name === "add") {
|
|
590
|
+
const a = request.params.arguments?.a;
|
|
591
|
+
const b = request.params.arguments?.b;
|
|
592
|
+
if (typeof a !== "number" || typeof b !== "number") {
|
|
593
|
+
throw new Error("Add tool requires numeric a and b arguments");
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: String(a + b),
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (request.params.name === "greet") {
|
|
605
|
+
const name = request.params.arguments?.name;
|
|
606
|
+
const formal = request.params.arguments?.formal;
|
|
607
|
+
if (typeof name !== "string") {
|
|
608
|
+
throw new Error("Greet tool requires a string name argument");
|
|
609
|
+
}
|
|
610
|
+
if (formal !== undefined && typeof formal !== "boolean") {
|
|
611
|
+
throw new Error("Greet tool formal argument must be boolean when provided");
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
content: [
|
|
615
|
+
{
|
|
616
|
+
type: "text",
|
|
617
|
+
text: formal ? `Good day, ${name}.` : `Hello, ${name}!`,
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
if (request.params.name === "fail") {
|
|
623
|
+
return {
|
|
624
|
+
isError: true,
|
|
625
|
+
content: [
|
|
626
|
+
{
|
|
627
|
+
type: "text",
|
|
628
|
+
text: "Intentional tool failure.",
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
634
|
+
});
|
|
635
|
+
return server;
|
|
636
|
+
}
|
|
637
|
+
export async function createMockPaginatedToolsServer() {
|
|
638
|
+
const [{ Server }, { ListToolsRequestSchema }] = await Promise.all([
|
|
639
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
640
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
641
|
+
]);
|
|
642
|
+
const server = new Server({ name: "mock-paginated-tools-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
643
|
+
const pageSize = 5;
|
|
644
|
+
const tools = Array.from({ length: 20 }, (_, index) => ({
|
|
645
|
+
name: `tool-${index + 1}`,
|
|
646
|
+
description: `Mock paginated tool ${index + 1}.`,
|
|
647
|
+
inputSchema: {
|
|
648
|
+
type: "object",
|
|
649
|
+
properties: {},
|
|
650
|
+
},
|
|
651
|
+
}));
|
|
652
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
653
|
+
const cursor = request.params?.cursor;
|
|
654
|
+
const startIndex = cursor === undefined ? 0 : Number(cursor);
|
|
655
|
+
if (!Number.isInteger(startIndex) ||
|
|
656
|
+
startIndex < 0 ||
|
|
657
|
+
startIndex > tools.length ||
|
|
658
|
+
startIndex % pageSize !== 0) {
|
|
659
|
+
throw new Error(`Invalid cursor: ${String(cursor)}`);
|
|
660
|
+
}
|
|
661
|
+
const pageTools = tools.slice(startIndex, startIndex + pageSize);
|
|
662
|
+
const nextIndex = startIndex + pageSize;
|
|
663
|
+
if (nextIndex >= tools.length) {
|
|
664
|
+
return { tools: pageTools };
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
tools: pageTools,
|
|
668
|
+
nextCursor: String(nextIndex),
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
return server;
|
|
672
|
+
}
|
|
673
|
+
export async function createMockResourceServer() {
|
|
674
|
+
const [{ Server }, { ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, },] = await Promise.all([
|
|
675
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
676
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
677
|
+
]);
|
|
678
|
+
const server = new Server({ name: "mock-resource-server", version: "1.0.0" }, { capabilities: { resources: {} } });
|
|
679
|
+
const resources = [
|
|
680
|
+
{
|
|
681
|
+
uri: "file:///readme.txt",
|
|
682
|
+
name: "readme.txt",
|
|
683
|
+
mimeType: "text/plain",
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
uri: "file:///image.png",
|
|
687
|
+
name: "image.png",
|
|
688
|
+
mimeType: "image/png",
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
const resourceContentsByUri = new Map([
|
|
692
|
+
[
|
|
693
|
+
"file:///readme.txt",
|
|
694
|
+
{
|
|
695
|
+
uri: "file:///readme.txt",
|
|
696
|
+
mimeType: "text/plain",
|
|
697
|
+
text: "This is a mock README resource.",
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
[
|
|
701
|
+
"file:///image.png",
|
|
702
|
+
{
|
|
703
|
+
uri: "file:///image.png",
|
|
704
|
+
mimeType: "image/png",
|
|
705
|
+
blob: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8Xw8AAoMBgL9qj3QAAAAASUVORK5CYII=",
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
]);
|
|
709
|
+
const resourceTemplates = [
|
|
710
|
+
{
|
|
711
|
+
uriTemplate: "file:///{path}",
|
|
712
|
+
name: "file-template",
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
716
|
+
resources,
|
|
717
|
+
}));
|
|
718
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
719
|
+
const resourceContent = resourceContentsByUri.get(request.params.uri);
|
|
720
|
+
if (resourceContent === undefined) {
|
|
721
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
contents: [resourceContent],
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
728
|
+
resourceTemplates,
|
|
729
|
+
}));
|
|
730
|
+
return server;
|
|
731
|
+
}
|
|
732
|
+
export async function createMockSubscribableResourceServer() {
|
|
733
|
+
const [{ Server }, { ListResourcesRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, },] = await Promise.all([
|
|
734
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
735
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
736
|
+
]);
|
|
737
|
+
const server = new Server({ name: "mock-subscribable-resource-server", version: "1.0.0" }, { capabilities: { resources: { subscribe: true, listChanged: true } } });
|
|
738
|
+
const resources = [
|
|
739
|
+
{
|
|
740
|
+
uri: "file:///readme.txt",
|
|
741
|
+
name: "readme.txt",
|
|
742
|
+
mimeType: "text/plain",
|
|
743
|
+
},
|
|
744
|
+
];
|
|
745
|
+
const resourceContentsByUri = new Map([
|
|
746
|
+
[
|
|
747
|
+
"file:///readme.txt",
|
|
748
|
+
{
|
|
749
|
+
uri: "file:///readme.txt",
|
|
750
|
+
mimeType: "text/plain",
|
|
751
|
+
text: "Initial subscribable resource text.",
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
]);
|
|
755
|
+
const subscribedUris = new Set();
|
|
756
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
757
|
+
resources,
|
|
758
|
+
}));
|
|
759
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
760
|
+
const resourceContent = resourceContentsByUri.get(request.params.uri);
|
|
761
|
+
if (resourceContent === undefined) {
|
|
762
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
contents: [resourceContent],
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
769
|
+
subscribedUris.add(request.params.uri);
|
|
770
|
+
return {};
|
|
771
|
+
});
|
|
772
|
+
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
773
|
+
subscribedUris.delete(request.params.uri);
|
|
774
|
+
return {};
|
|
775
|
+
});
|
|
776
|
+
return Object.assign(server, {
|
|
777
|
+
triggerResourceUpdated: async (uri, updatedText) => {
|
|
778
|
+
if (updatedText !== undefined) {
|
|
779
|
+
const existingContent = resourceContentsByUri.get(uri);
|
|
780
|
+
if (existingContent === undefined || "blob" in existingContent) {
|
|
781
|
+
throw new Error(`Unknown text resource: ${uri}`);
|
|
782
|
+
}
|
|
783
|
+
resourceContentsByUri.set(uri, {
|
|
784
|
+
uri,
|
|
785
|
+
mimeType: existingContent.mimeType,
|
|
786
|
+
text: updatedText,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
if (!subscribedUris.has(uri)) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
await server.sendResourceUpdated({ uri });
|
|
793
|
+
},
|
|
794
|
+
triggerResourceListChanged: async () => {
|
|
795
|
+
await server.sendResourceListChanged();
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
export async function createMockPromptServer() {
|
|
800
|
+
const [{ Server }, { ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, McpError: SdkMcpError },] = await Promise.all([
|
|
801
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
802
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
803
|
+
]);
|
|
804
|
+
const server = new Server({ name: "mock-prompt-server", version: "1.0.0" }, { capabilities: { prompts: {} } });
|
|
805
|
+
const promptTemplates = [
|
|
806
|
+
{
|
|
807
|
+
prompt: {
|
|
808
|
+
name: "code_review",
|
|
809
|
+
description: "Review code for correctness and maintainability.",
|
|
810
|
+
arguments: [
|
|
811
|
+
{
|
|
812
|
+
name: "code",
|
|
813
|
+
description: "Code to review.",
|
|
814
|
+
required: true,
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
},
|
|
818
|
+
messages: [
|
|
819
|
+
{
|
|
820
|
+
role: "user",
|
|
821
|
+
textTemplate: "Please review the following code:\n{{code}}",
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
role: "assistant",
|
|
825
|
+
textTemplate: "I will review the code for potential issues and improvements.",
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
prompt: {
|
|
831
|
+
name: "summarize",
|
|
832
|
+
description: "Summarize the provided text.",
|
|
833
|
+
},
|
|
834
|
+
messages: [
|
|
835
|
+
{
|
|
836
|
+
role: "user",
|
|
837
|
+
textTemplate: "Please summarize the provided text.",
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
},
|
|
841
|
+
];
|
|
842
|
+
const promptsByName = new Map(promptTemplates.map((template) => [template.prompt.name, template]));
|
|
843
|
+
const renderPromptMessage = (textTemplate, argumentsMap) => {
|
|
844
|
+
if (argumentsMap === undefined) {
|
|
845
|
+
return textTemplate;
|
|
846
|
+
}
|
|
847
|
+
let renderedText = textTemplate;
|
|
848
|
+
for (const [name, value] of Object.entries(argumentsMap)) {
|
|
849
|
+
if (typeof value !== "string") {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
renderedText = renderedText.split(`{{${name}}}`).join(value);
|
|
853
|
+
}
|
|
854
|
+
return renderedText;
|
|
855
|
+
};
|
|
856
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
857
|
+
prompts: promptTemplates.map((template) => template.prompt),
|
|
858
|
+
}));
|
|
859
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
860
|
+
const template = promptsByName.get(request.params.name);
|
|
861
|
+
if (template === undefined) {
|
|
862
|
+
throw new SdkMcpError(ErrorCode.InvalidParams, `Unknown prompt: ${request.params.name}`);
|
|
863
|
+
}
|
|
864
|
+
const requestArguments = request.params.arguments;
|
|
865
|
+
for (const argument of template.prompt.arguments ?? []) {
|
|
866
|
+
if (!argument.required) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const value = requestArguments?.[argument.name];
|
|
870
|
+
if (typeof value !== "string") {
|
|
871
|
+
throw new SdkMcpError(ErrorCode.InvalidParams, `Missing required prompt argument: ${argument.name}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
description: template.prompt.description,
|
|
876
|
+
messages: template.messages.map((message) => ({
|
|
877
|
+
role: message.role,
|
|
878
|
+
content: {
|
|
879
|
+
type: "text",
|
|
880
|
+
text: renderPromptMessage(message.textTemplate, requestArguments),
|
|
881
|
+
},
|
|
882
|
+
})),
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
return server;
|
|
886
|
+
}
|
|
887
|
+
export async function createMockCompletionServer() {
|
|
888
|
+
const [{ Server }, { CompleteRequestSchema }] = await Promise.all([
|
|
889
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
890
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
891
|
+
]);
|
|
892
|
+
const server = new Server({ name: "mock-completion-server", version: "1.0.0" }, { capabilities: { completions: {} } });
|
|
893
|
+
const promptArgumentCompletions = new Map([
|
|
894
|
+
["code_review:language", ["python", "pydantic", "pytest", "pytorch", "pyright", "rust"]],
|
|
895
|
+
]);
|
|
896
|
+
const maxValues = 3;
|
|
897
|
+
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
898
|
+
if (request.params.ref.type !== "ref/prompt") {
|
|
899
|
+
return {
|
|
900
|
+
completion: {
|
|
901
|
+
values: [],
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
const completionKey = `${request.params.ref.name}:${request.params.argument.name}`;
|
|
906
|
+
const candidates = promptArgumentCompletions.get(completionKey) ?? [];
|
|
907
|
+
const partialValue = request.params.argument.value.toLowerCase();
|
|
908
|
+
const matchingValues = candidates.filter((candidate) => candidate.toLowerCase().startsWith(partialValue));
|
|
909
|
+
const values = matchingValues.slice(0, maxValues);
|
|
910
|
+
if (values.length < matchingValues.length) {
|
|
911
|
+
return {
|
|
912
|
+
completion: {
|
|
913
|
+
values,
|
|
914
|
+
hasMore: true,
|
|
915
|
+
total: matchingValues.length,
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
completion: {
|
|
921
|
+
values,
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
});
|
|
925
|
+
return server;
|
|
926
|
+
}
|
|
927
|
+
export async function createMockProgressServer() {
|
|
928
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
929
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
930
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
931
|
+
]);
|
|
932
|
+
const server = new Server({ name: "mock-progress-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
933
|
+
const totalSteps = 4;
|
|
934
|
+
const tool = {
|
|
935
|
+
name: "slow_task",
|
|
936
|
+
description: "Runs a simulated slow task and streams progress updates.",
|
|
937
|
+
inputSchema: {
|
|
938
|
+
type: "object",
|
|
939
|
+
properties: {},
|
|
940
|
+
additionalProperties: false,
|
|
941
|
+
},
|
|
942
|
+
};
|
|
943
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
944
|
+
tools: [tool],
|
|
945
|
+
}));
|
|
946
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
947
|
+
if (request.params.name !== "slow_task") {
|
|
948
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
949
|
+
}
|
|
950
|
+
const progressToken = request.params._meta?.progressToken;
|
|
951
|
+
if (progressToken !== undefined) {
|
|
952
|
+
for (let step = 1; step <= totalSteps; step += 1) {
|
|
953
|
+
await extra.sendNotification({
|
|
954
|
+
method: "notifications/progress",
|
|
955
|
+
params: {
|
|
956
|
+
progressToken,
|
|
957
|
+
progress: step,
|
|
958
|
+
total: totalSteps,
|
|
959
|
+
message: `Completed step ${step} of ${totalSteps}`,
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
await new Promise((resolve) => {
|
|
963
|
+
setTimeout(resolve, 5);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
content: [
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: "slow_task complete",
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
};
|
|
975
|
+
});
|
|
976
|
+
return server;
|
|
977
|
+
}
|
|
978
|
+
export async function createMockSlowToolServer(options = {}) {
|
|
979
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
980
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
981
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
982
|
+
]);
|
|
983
|
+
const defaultDelayMs = options.delayMs ?? 1_000;
|
|
984
|
+
const pollIntervalMs = options.pollIntervalMs ?? 10;
|
|
985
|
+
if (!Number.isFinite(defaultDelayMs) || defaultDelayMs < 0) {
|
|
986
|
+
throw new Error("createMockSlowToolServer delayMs must be a finite non-negative number");
|
|
987
|
+
}
|
|
988
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
|
|
989
|
+
throw new Error("createMockSlowToolServer pollIntervalMs must be a finite positive number");
|
|
990
|
+
}
|
|
991
|
+
const server = new Server({ name: "mock-slow-tool-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
992
|
+
const startedRequestIds = new Set();
|
|
993
|
+
const cancelledRequestIds = new Set();
|
|
994
|
+
const tool = {
|
|
995
|
+
name: "slow",
|
|
996
|
+
description: "Delays the response and supports cancellation.",
|
|
997
|
+
inputSchema: {
|
|
998
|
+
type: "object",
|
|
999
|
+
properties: {
|
|
1000
|
+
delayMs: {
|
|
1001
|
+
type: "number",
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
additionalProperties: false,
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1008
|
+
tools: [tool],
|
|
1009
|
+
}));
|
|
1010
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1011
|
+
if (request.params.name !== "slow") {
|
|
1012
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1013
|
+
}
|
|
1014
|
+
startedRequestIds.add(extra.requestId);
|
|
1015
|
+
const delayArgument = request.params.arguments?.delayMs;
|
|
1016
|
+
const delayMs = delayArgument === undefined
|
|
1017
|
+
? defaultDelayMs
|
|
1018
|
+
: typeof delayArgument === "number" && Number.isFinite(delayArgument) && delayArgument >= 0
|
|
1019
|
+
? delayArgument
|
|
1020
|
+
: NaN;
|
|
1021
|
+
if (!Number.isFinite(delayMs)) {
|
|
1022
|
+
throw new Error("slow tool delayMs argument must be a finite non-negative number");
|
|
1023
|
+
}
|
|
1024
|
+
const startedAt = Date.now();
|
|
1025
|
+
while (Date.now() - startedAt < delayMs) {
|
|
1026
|
+
if (extra.signal.aborted) {
|
|
1027
|
+
cancelledRequestIds.add(extra.requestId);
|
|
1028
|
+
throw new Error("slow tool cancelled");
|
|
1029
|
+
}
|
|
1030
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1031
|
+
const remainingMs = delayMs - elapsedMs;
|
|
1032
|
+
await new Promise((resolve) => {
|
|
1033
|
+
setTimeout(resolve, Math.max(0, Math.min(pollIntervalMs, remainingMs)));
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (extra.signal.aborted) {
|
|
1037
|
+
cancelledRequestIds.add(extra.requestId);
|
|
1038
|
+
throw new Error("slow tool cancelled");
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
content: [
|
|
1042
|
+
{
|
|
1043
|
+
type: "text",
|
|
1044
|
+
text: `slow complete after ${delayMs}ms`,
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
};
|
|
1048
|
+
});
|
|
1049
|
+
return Object.assign(server, {
|
|
1050
|
+
wasStarted: () => startedRequestIds.size > 0,
|
|
1051
|
+
getStartedRequestIds: () => Array.from(startedRequestIds),
|
|
1052
|
+
wasCancelled: () => cancelledRequestIds.size > 0,
|
|
1053
|
+
getCancelledRequestIds: () => Array.from(cancelledRequestIds),
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
export async function createMockErrorServer() {
|
|
1057
|
+
const [{ Server }, { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError: SdkMcpError },] = await Promise.all([
|
|
1058
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1059
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
1060
|
+
]);
|
|
1061
|
+
const server = new Server({ name: "mock-error-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
1062
|
+
const tools = [
|
|
1063
|
+
{
|
|
1064
|
+
name: "invalid_params",
|
|
1065
|
+
description: "Returns a JSON-RPC Invalid Params error.",
|
|
1066
|
+
inputSchema: {
|
|
1067
|
+
type: "object",
|
|
1068
|
+
properties: {},
|
|
1069
|
+
additionalProperties: false,
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: "is_error",
|
|
1074
|
+
description: "Returns a tools/call result with isError: true.",
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
type: "object",
|
|
1077
|
+
properties: {},
|
|
1078
|
+
additionalProperties: false,
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: "internal_error",
|
|
1083
|
+
description: "Throws an internal server error.",
|
|
1084
|
+
inputSchema: {
|
|
1085
|
+
type: "object",
|
|
1086
|
+
properties: {},
|
|
1087
|
+
additionalProperties: false,
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
];
|
|
1091
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1092
|
+
tools,
|
|
1093
|
+
}));
|
|
1094
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1095
|
+
if (request.params.name === "invalid_params") {
|
|
1096
|
+
throw new SdkMcpError(ErrorCode.InvalidParams, "Intentional invalid params error from mock-error-server.");
|
|
1097
|
+
}
|
|
1098
|
+
if (request.params.name === "is_error") {
|
|
1099
|
+
return {
|
|
1100
|
+
isError: true,
|
|
1101
|
+
content: [
|
|
1102
|
+
{
|
|
1103
|
+
type: "text",
|
|
1104
|
+
text: "Intentional isError tool failure.",
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
if (request.params.name === "internal_error") {
|
|
1110
|
+
throw new Error("Intentional internal error from mock-error-server.");
|
|
1111
|
+
}
|
|
1112
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1113
|
+
});
|
|
1114
|
+
return server;
|
|
1115
|
+
}
|
|
1116
|
+
export async function createMockSamplingServer() {
|
|
1117
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
1118
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1119
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
1120
|
+
]);
|
|
1121
|
+
const server = new Server({ name: "mock-sampling-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
1122
|
+
const tool = {
|
|
1123
|
+
name: "sample_message",
|
|
1124
|
+
description: "Requests sampling/createMessage from the client and returns the sampled text.",
|
|
1125
|
+
inputSchema: {
|
|
1126
|
+
type: "object",
|
|
1127
|
+
properties: {
|
|
1128
|
+
topic: {
|
|
1129
|
+
type: "string",
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
required: ["topic"],
|
|
1133
|
+
additionalProperties: false,
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1137
|
+
tools: [tool],
|
|
1138
|
+
}));
|
|
1139
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1140
|
+
if (request.params.name !== "sample_message") {
|
|
1141
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1142
|
+
}
|
|
1143
|
+
const topic = request.params.arguments?.topic;
|
|
1144
|
+
if (typeof topic !== "string") {
|
|
1145
|
+
throw new Error("sample_message requires a string topic argument");
|
|
1146
|
+
}
|
|
1147
|
+
const samplingResult = await server.createMessage({
|
|
1148
|
+
messages: [
|
|
1149
|
+
{
|
|
1150
|
+
role: "user",
|
|
1151
|
+
content: {
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: `Provide a concise sentence about ${topic}.`,
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
],
|
|
1157
|
+
maxTokens: 64,
|
|
1158
|
+
modelPreferences: {
|
|
1159
|
+
hints: [{ name: "mock-sampling-model" }],
|
|
1160
|
+
speedPriority: 0.2,
|
|
1161
|
+
intelligencePriority: 0.9,
|
|
1162
|
+
},
|
|
1163
|
+
systemPrompt: "Return exactly one concise sentence.",
|
|
1164
|
+
});
|
|
1165
|
+
const sampledText = samplingResult.content.type === "text"
|
|
1166
|
+
? samplingResult.content.text
|
|
1167
|
+
: JSON.stringify(samplingResult.content);
|
|
1168
|
+
return {
|
|
1169
|
+
content: [
|
|
1170
|
+
{
|
|
1171
|
+
type: "text",
|
|
1172
|
+
text: `Sampled response: ${sampledText}`,
|
|
1173
|
+
},
|
|
1174
|
+
],
|
|
1175
|
+
};
|
|
1176
|
+
});
|
|
1177
|
+
return server;
|
|
1178
|
+
}
|
|
1179
|
+
export async function createMockRootsServer() {
|
|
1180
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema, RootsListChangedNotificationSchema, },] = await Promise.all([
|
|
1181
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1182
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
1183
|
+
]);
|
|
1184
|
+
const server = new Server({ name: "mock-roots-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
1185
|
+
const tool = {
|
|
1186
|
+
name: "roots_summary",
|
|
1187
|
+
description: "Requests roots/list from the client and returns a summary of roots.",
|
|
1188
|
+
inputSchema: {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {},
|
|
1191
|
+
additionalProperties: false,
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1195
|
+
tools: [tool],
|
|
1196
|
+
}));
|
|
1197
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
1198
|
+
await server.listRoots();
|
|
1199
|
+
});
|
|
1200
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1201
|
+
if (request.params.name !== "roots_summary") {
|
|
1202
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1203
|
+
}
|
|
1204
|
+
const rootsResult = await server.listRoots();
|
|
1205
|
+
const rootSummary = rootsResult.roots
|
|
1206
|
+
.map((root) => (root.name === undefined ? root.uri : `${root.name} (${root.uri})`))
|
|
1207
|
+
.join(", ");
|
|
1208
|
+
return {
|
|
1209
|
+
content: [
|
|
1210
|
+
{
|
|
1211
|
+
type: "text",
|
|
1212
|
+
text: rootSummary.length === 0 ? "Roots: none" : `Roots: ${rootSummary}`,
|
|
1213
|
+
},
|
|
1214
|
+
],
|
|
1215
|
+
};
|
|
1216
|
+
});
|
|
1217
|
+
return server;
|
|
1218
|
+
}
|
|
1219
|
+
export async function createMockLoggingServer() {
|
|
1220
|
+
const [{ Server }, { CallToolRequestSchema, ListToolsRequestSchema }] = await Promise.all([
|
|
1221
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1222
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
1223
|
+
]);
|
|
1224
|
+
const server = new Server({ name: "mock-logging-server", version: "1.0.0" }, { capabilities: { tools: {}, logging: {} } });
|
|
1225
|
+
const tool = {
|
|
1226
|
+
name: "emit_logs",
|
|
1227
|
+
description: "Emits mock log messages at multiple levels.",
|
|
1228
|
+
inputSchema: {
|
|
1229
|
+
type: "object",
|
|
1230
|
+
properties: {},
|
|
1231
|
+
additionalProperties: false,
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1235
|
+
tools: [tool],
|
|
1236
|
+
}));
|
|
1237
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1238
|
+
if (request.params.name !== "emit_logs") {
|
|
1239
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1240
|
+
}
|
|
1241
|
+
await server.sendLoggingMessage({
|
|
1242
|
+
level: "debug",
|
|
1243
|
+
logger: "mock-logging-server",
|
|
1244
|
+
data: { message: "Debug message" },
|
|
1245
|
+
});
|
|
1246
|
+
await server.sendLoggingMessage({
|
|
1247
|
+
level: "info",
|
|
1248
|
+
logger: "mock-logging-server",
|
|
1249
|
+
data: { message: "Info message" },
|
|
1250
|
+
});
|
|
1251
|
+
await server.sendLoggingMessage({
|
|
1252
|
+
level: "error",
|
|
1253
|
+
logger: "mock-logging-server",
|
|
1254
|
+
data: { message: "Error message" },
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
content: [
|
|
1258
|
+
{
|
|
1259
|
+
type: "text",
|
|
1260
|
+
text: "Emitted log messages.",
|
|
1261
|
+
},
|
|
1262
|
+
],
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
return server;
|
|
1266
|
+
}
|
|
1267
|
+
export async function createMockFullFeaturedServer() {
|
|
1268
|
+
const [{ Server }, { CallToolRequestSchema, CompleteRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, },] = await Promise.all([
|
|
1269
|
+
import("@modelcontextprotocol/sdk/server/index.js"),
|
|
1270
|
+
import("@modelcontextprotocol/sdk/types.js"),
|
|
1271
|
+
]);
|
|
1272
|
+
const server = new Server({ name: "mock-full-featured-server", version: "1.0.0" }, {
|
|
1273
|
+
capabilities: {
|
|
1274
|
+
tools: {},
|
|
1275
|
+
resources: {},
|
|
1276
|
+
prompts: {},
|
|
1277
|
+
logging: {},
|
|
1278
|
+
completions: {},
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
const tool = {
|
|
1282
|
+
name: "full_featured_ping",
|
|
1283
|
+
description: "Returns a text response and emits an info log.",
|
|
1284
|
+
inputSchema: {
|
|
1285
|
+
type: "object",
|
|
1286
|
+
properties: {},
|
|
1287
|
+
additionalProperties: false,
|
|
1288
|
+
},
|
|
1289
|
+
};
|
|
1290
|
+
const resources = [
|
|
1291
|
+
{
|
|
1292
|
+
uri: "file:///full-featured.txt",
|
|
1293
|
+
name: "full-featured.txt",
|
|
1294
|
+
mimeType: "text/plain",
|
|
1295
|
+
},
|
|
1296
|
+
];
|
|
1297
|
+
const prompts = [
|
|
1298
|
+
{
|
|
1299
|
+
name: "full_featured_prompt",
|
|
1300
|
+
description: "Returns a short prompt message for a topic.",
|
|
1301
|
+
arguments: [
|
|
1302
|
+
{
|
|
1303
|
+
name: "topic",
|
|
1304
|
+
description: "Topic to include in the prompt output.",
|
|
1305
|
+
required: false,
|
|
1306
|
+
},
|
|
1307
|
+
],
|
|
1308
|
+
},
|
|
1309
|
+
];
|
|
1310
|
+
const completionValues = ["alpha", "beta", "gamma"];
|
|
1311
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1312
|
+
tools: [tool],
|
|
1313
|
+
}));
|
|
1314
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1315
|
+
if (request.params.name !== "full_featured_ping") {
|
|
1316
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1317
|
+
}
|
|
1318
|
+
await server.sendLoggingMessage({
|
|
1319
|
+
level: "info",
|
|
1320
|
+
logger: "mock-full-featured-server",
|
|
1321
|
+
data: {
|
|
1322
|
+
message: "full_featured_ping called",
|
|
1323
|
+
},
|
|
1324
|
+
});
|
|
1325
|
+
return {
|
|
1326
|
+
content: [
|
|
1327
|
+
{
|
|
1328
|
+
type: "text",
|
|
1329
|
+
text: "full_featured_ping ok",
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
};
|
|
1333
|
+
});
|
|
1334
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
1335
|
+
resources,
|
|
1336
|
+
}));
|
|
1337
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1338
|
+
if (request.params.uri !== "file:///full-featured.txt") {
|
|
1339
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
contents: [
|
|
1343
|
+
{
|
|
1344
|
+
uri: "file:///full-featured.txt",
|
|
1345
|
+
mimeType: "text/plain",
|
|
1346
|
+
text: "Mock full-featured resource",
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
};
|
|
1350
|
+
});
|
|
1351
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1352
|
+
prompts,
|
|
1353
|
+
}));
|
|
1354
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1355
|
+
if (request.params.name !== "full_featured_prompt") {
|
|
1356
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
1357
|
+
}
|
|
1358
|
+
const topic = request.params.arguments?.topic;
|
|
1359
|
+
const topicText = typeof topic === "string" && topic.length > 0 ? topic : "general";
|
|
1360
|
+
return {
|
|
1361
|
+
description: "Mock prompt from full-featured server.",
|
|
1362
|
+
messages: [
|
|
1363
|
+
{
|
|
1364
|
+
role: "user",
|
|
1365
|
+
content: {
|
|
1366
|
+
type: "text",
|
|
1367
|
+
text: `Provide a short summary for ${topicText}.`,
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
],
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
1374
|
+
if (request.params.ref.type !== "ref/prompt" ||
|
|
1375
|
+
request.params.ref.name !== "full_featured_prompt" ||
|
|
1376
|
+
request.params.argument.name !== "topic") {
|
|
1377
|
+
return {
|
|
1378
|
+
completion: {
|
|
1379
|
+
values: [],
|
|
1380
|
+
},
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
const partialValue = request.params.argument.value.toLowerCase();
|
|
1384
|
+
const values = completionValues.filter((value) => value.startsWith(partialValue));
|
|
1385
|
+
return {
|
|
1386
|
+
completion: {
|
|
1387
|
+
values,
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
return server;
|
|
1392
|
+
}
|
|
1393
|
+
export async function createTestPair(server, createClient) {
|
|
1394
|
+
const { clientTransport, serverTransport } = createInMemoryTransportPair();
|
|
1395
|
+
const serverPromise = server.connect(serverTransport);
|
|
1396
|
+
const client = createClient();
|
|
1397
|
+
try {
|
|
1398
|
+
await client.connect(clientTransport);
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
clientTransport.dispose(new Error("tiny-stdio-mcp-server test pair setup failed"));
|
|
1402
|
+
await serverPromise;
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
const cleanup = async () => {
|
|
1406
|
+
await client.close();
|
|
1407
|
+
clientTransport.dispose(new Error("tiny-stdio-mcp-server test pair cleanup"));
|
|
1408
|
+
await serverPromise;
|
|
1409
|
+
};
|
|
1410
|
+
return { client, cleanup };
|
|
1411
|
+
}
|
|
1412
|
+
function defaultStdioSpawn(command, args, options) {
|
|
1413
|
+
return spawn(command, args, options);
|
|
1414
|
+
}
|
|
1415
|
+
function defaultHttpTransportFetch(input, init) {
|
|
1416
|
+
return fetch(input, init);
|
|
1417
|
+
}
|
|
1418
|
+
export class StdioTransport {
|
|
1419
|
+
readable;
|
|
1420
|
+
writable;
|
|
1421
|
+
closed;
|
|
1422
|
+
child;
|
|
1423
|
+
disposed = false;
|
|
1424
|
+
stderrOutput = "";
|
|
1425
|
+
static STDERR_MAX_LENGTH = 65_536;
|
|
1426
|
+
constructor({ command, args = [], cwd, env, spawn: spawnProcess = defaultStdioSpawn, }) {
|
|
1427
|
+
this.child = spawnProcess(command, args, {
|
|
1428
|
+
cwd,
|
|
1429
|
+
env,
|
|
1430
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1431
|
+
});
|
|
1432
|
+
const child = this.child;
|
|
1433
|
+
this.readable = child.stdout;
|
|
1434
|
+
this.writable = child.stdin;
|
|
1435
|
+
child.stderr.on("data", (chunk) => {
|
|
1436
|
+
this.stderrOutput += chunkToString(chunk);
|
|
1437
|
+
if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
|
|
1438
|
+
this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
this.closed = new Promise((resolve) => {
|
|
1442
|
+
let settled = false;
|
|
1443
|
+
const resolveClosed = (event) => {
|
|
1444
|
+
if (settled) {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
settled = true;
|
|
1448
|
+
resolve(event);
|
|
1449
|
+
};
|
|
1450
|
+
child.once("exit", (code, signal) => {
|
|
1451
|
+
const closedEvent = {
|
|
1452
|
+
reason: new Error("Stdio transport process exited"),
|
|
1453
|
+
};
|
|
1454
|
+
if (code !== null) {
|
|
1455
|
+
closedEvent.code = code;
|
|
1456
|
+
}
|
|
1457
|
+
if (signal !== null) {
|
|
1458
|
+
closedEvent.signal = signal;
|
|
1459
|
+
}
|
|
1460
|
+
resolveClosed(closedEvent);
|
|
1461
|
+
});
|
|
1462
|
+
child.once("error", (error) => {
|
|
1463
|
+
const closedEvent = {
|
|
1464
|
+
reason: error instanceof Error ? error : new Error(String(error)),
|
|
1465
|
+
};
|
|
1466
|
+
if (child.exitCode !== null) {
|
|
1467
|
+
closedEvent.code = child.exitCode;
|
|
1468
|
+
}
|
|
1469
|
+
if (child.signalCode !== null) {
|
|
1470
|
+
closedEvent.signal = child.signalCode;
|
|
1471
|
+
}
|
|
1472
|
+
resolveClosed(closedEvent);
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
getStderrOutput() {
|
|
1477
|
+
return this.stderrOutput;
|
|
1478
|
+
}
|
|
1479
|
+
dispose(reason = new Error("Stdio transport disposed")) {
|
|
1480
|
+
void reason;
|
|
1481
|
+
if (this.disposed) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
this.disposed = true;
|
|
1485
|
+
if (!this.child.stdin.destroyed && !this.child.stdin.writableEnded) {
|
|
1486
|
+
this.child.stdin.end();
|
|
1487
|
+
}
|
|
1488
|
+
if (this.child.exitCode === null && this.child.signalCode === null && !this.child.killed) {
|
|
1489
|
+
this.child.kill("SIGTERM");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
export class HttpTransport {
|
|
1494
|
+
readable;
|
|
1495
|
+
writable;
|
|
1496
|
+
closed;
|
|
1497
|
+
url;
|
|
1498
|
+
headers;
|
|
1499
|
+
fetchImpl;
|
|
1500
|
+
readStream = new PassThrough();
|
|
1501
|
+
writeStream = new PassThrough();
|
|
1502
|
+
resolveClosed;
|
|
1503
|
+
sessionId;
|
|
1504
|
+
lastEventId;
|
|
1505
|
+
getSseStreamStarted = false;
|
|
1506
|
+
disposed = false;
|
|
1507
|
+
oauthProvider;
|
|
1508
|
+
oauthMetadataDiscovery;
|
|
1509
|
+
inFlightFetchAbortControllers = new Set();
|
|
1510
|
+
openSseReaders = new Set();
|
|
1511
|
+
constructor({ url, headers = {}, fetch: fetchImpl = defaultHttpTransportFetch, oauth, oauthDiscoveryCache, }) {
|
|
1512
|
+
this.url = url;
|
|
1513
|
+
this.headers = headers;
|
|
1514
|
+
this.fetchImpl = fetchImpl;
|
|
1515
|
+
this.oauthProvider = oauth === undefined
|
|
1516
|
+
? undefined
|
|
1517
|
+
: createOAuthClientProvider(oauth);
|
|
1518
|
+
this.oauthMetadataDiscovery = oauth === undefined
|
|
1519
|
+
? undefined
|
|
1520
|
+
: new OAuthMetadataDiscovery({
|
|
1521
|
+
fetch: (input, init) => this.fetchWithAbort(input, init ?? {}),
|
|
1522
|
+
cache: oauthDiscoveryCache,
|
|
1523
|
+
});
|
|
1524
|
+
this.readable = this.readStream;
|
|
1525
|
+
this.writable = this.writeStream;
|
|
1526
|
+
this.closed = new Promise((resolve) => {
|
|
1527
|
+
this.resolveClosed = resolve;
|
|
1528
|
+
});
|
|
1529
|
+
this.readStream.once("error", (error) => {
|
|
1530
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
1531
|
+
});
|
|
1532
|
+
this.writeStream.once("error", (error) => {
|
|
1533
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
1534
|
+
});
|
|
1535
|
+
this.consumeWrittenLines().catch((error) => {
|
|
1536
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
dispose(reason = new Error("HTTP transport disposed")) {
|
|
1540
|
+
if (this.disposed) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
this.disposed = true;
|
|
1544
|
+
this.abortInFlightFetches();
|
|
1545
|
+
this.cancelOpenSseReaders();
|
|
1546
|
+
this.terminateSession();
|
|
1547
|
+
if (!this.writeStream.destroyed && !this.writeStream.writableEnded) {
|
|
1548
|
+
this.writeStream.end();
|
|
1549
|
+
}
|
|
1550
|
+
if (!this.readStream.destroyed && !this.readStream.writableEnded) {
|
|
1551
|
+
this.readStream.end();
|
|
1552
|
+
}
|
|
1553
|
+
const resolveClosed = this.resolveClosed;
|
|
1554
|
+
this.resolveClosed = undefined;
|
|
1555
|
+
resolveClosed?.({ reason });
|
|
1556
|
+
}
|
|
1557
|
+
abortInFlightFetches() {
|
|
1558
|
+
for (const abortController of this.inFlightFetchAbortControllers) {
|
|
1559
|
+
abortController.abort();
|
|
1560
|
+
}
|
|
1561
|
+
this.inFlightFetchAbortControllers.clear();
|
|
1562
|
+
}
|
|
1563
|
+
cancelOpenSseReaders() {
|
|
1564
|
+
for (const reader of this.openSseReaders) {
|
|
1565
|
+
void reader.cancel().catch(() => undefined);
|
|
1566
|
+
}
|
|
1567
|
+
this.openSseReaders.clear();
|
|
1568
|
+
}
|
|
1569
|
+
async fetchWithAbort(input, init) {
|
|
1570
|
+
const abortController = new AbortController();
|
|
1571
|
+
this.inFlightFetchAbortControllers.add(abortController);
|
|
1572
|
+
try {
|
|
1573
|
+
return await this.fetchImpl(input, {
|
|
1574
|
+
...init,
|
|
1575
|
+
signal: abortController.signal,
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
finally {
|
|
1579
|
+
this.inFlightFetchAbortControllers.delete(abortController);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async consumeWrittenLines() {
|
|
1583
|
+
for await (const line of readLines(this.writeStream)) {
|
|
1584
|
+
if (this.disposed || line.length === 0) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const hasSessionId = this.sessionId !== undefined;
|
|
1588
|
+
const response = await this.fetchWithOAuthRetry({
|
|
1589
|
+
method: "POST",
|
|
1590
|
+
createHeaders: () => this.createPostHeaders(),
|
|
1591
|
+
body: line,
|
|
1592
|
+
});
|
|
1593
|
+
if (hasSessionId && response.status === 404) {
|
|
1594
|
+
this.sessionId = undefined;
|
|
1595
|
+
this.dispose(new Error("HTTP transport session expired (404 response)"));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
await this.throwForPostHttpError(response);
|
|
1599
|
+
this.captureSessionId(response);
|
|
1600
|
+
this.maybeOpenGetSseStream();
|
|
1601
|
+
await this.forwardResponseMessages(response);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
async createPostHeaders() {
|
|
1605
|
+
const headers = new Headers(this.headers);
|
|
1606
|
+
headers.set("Accept", "application/json, text/event-stream");
|
|
1607
|
+
headers.set("Content-Type", "application/json");
|
|
1608
|
+
if (this.sessionId !== undefined) {
|
|
1609
|
+
headers.set("Mcp-Session-Id", this.sessionId);
|
|
1610
|
+
}
|
|
1611
|
+
return this.authorizeRequestHeaders(headers);
|
|
1612
|
+
}
|
|
1613
|
+
async createGetHeaders() {
|
|
1614
|
+
const headers = new Headers(this.headers);
|
|
1615
|
+
headers.set("Accept", "text/event-stream");
|
|
1616
|
+
if (this.sessionId !== undefined) {
|
|
1617
|
+
headers.set("Mcp-Session-Id", this.sessionId);
|
|
1618
|
+
}
|
|
1619
|
+
if (this.lastEventId !== undefined) {
|
|
1620
|
+
headers.set("Last-Event-ID", this.lastEventId);
|
|
1621
|
+
}
|
|
1622
|
+
return this.authorizeRequestHeaders(headers);
|
|
1623
|
+
}
|
|
1624
|
+
async createDeleteHeaders(sessionId) {
|
|
1625
|
+
const headers = new Headers(this.headers);
|
|
1626
|
+
headers.set("Mcp-Session-Id", sessionId);
|
|
1627
|
+
return this.authorizeRequestHeaders(headers);
|
|
1628
|
+
}
|
|
1629
|
+
async authorizeRequestHeaders(headers) {
|
|
1630
|
+
await this.oauthProvider?.authorizeRequest?.({
|
|
1631
|
+
requestUrl: new URL(this.url),
|
|
1632
|
+
headers,
|
|
1633
|
+
fetch: this.fetchImpl,
|
|
1634
|
+
});
|
|
1635
|
+
return headers;
|
|
1636
|
+
}
|
|
1637
|
+
captureSessionId(response) {
|
|
1638
|
+
const sessionId = response.headers.get("Mcp-Session-Id");
|
|
1639
|
+
if (sessionId === null || sessionId.length === 0) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
this.sessionId = sessionId;
|
|
1643
|
+
}
|
|
1644
|
+
maybeOpenGetSseStream() {
|
|
1645
|
+
if (this.disposed || this.sessionId === undefined || this.getSseStreamStarted) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
this.getSseStreamStarted = true;
|
|
1649
|
+
this.consumeGetSseStream().catch((error) => {
|
|
1650
|
+
if (error instanceof HttpTransportGetSseNotSupportedError || this.disposed) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
this.dispose(error instanceof Error ? error : new Error(String(error)));
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
terminateSession() {
|
|
1657
|
+
if (this.sessionId === undefined) {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const sessionId = this.sessionId;
|
|
1661
|
+
this.sessionId = undefined;
|
|
1662
|
+
this.sendSessionTerminationRequest(sessionId).catch(() => undefined);
|
|
1663
|
+
}
|
|
1664
|
+
async sendSessionTerminationRequest(sessionId) {
|
|
1665
|
+
const response = await this.fetchImpl(this.url, {
|
|
1666
|
+
method: "DELETE",
|
|
1667
|
+
headers: await this.createDeleteHeaders(sessionId),
|
|
1668
|
+
});
|
|
1669
|
+
if (response.status === 405) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async consumeGetSseStream() {
|
|
1674
|
+
const response = await this.fetchWithOAuthRetry({
|
|
1675
|
+
method: "GET",
|
|
1676
|
+
createHeaders: () => this.createGetHeaders(),
|
|
1677
|
+
});
|
|
1678
|
+
if (response.status === 405) {
|
|
1679
|
+
throw new HttpTransportGetSseNotSupportedError();
|
|
1680
|
+
}
|
|
1681
|
+
const contentType = response.headers.get("Content-Type");
|
|
1682
|
+
if (contentType === null) {
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (contentType.toLowerCase().includes("text/event-stream")) {
|
|
1686
|
+
await this.forwardSseResponseMessages(response);
|
|
1687
|
+
this.getSseStreamStarted = false;
|
|
1688
|
+
if (!this.disposed && this.sessionId !== undefined && this.lastEventId !== undefined) {
|
|
1689
|
+
this.maybeOpenGetSseStream();
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
async throwForPostHttpError(response) {
|
|
1694
|
+
if (response.status < 400) {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const responseBody = (await response.text()).trim();
|
|
1698
|
+
const statusDescriptor = `${response.status} ${response.statusText}`.trim();
|
|
1699
|
+
const message = responseBody.length === 0
|
|
1700
|
+
? `HTTP transport POST failed (${statusDescriptor})`
|
|
1701
|
+
: `HTTP transport POST failed (${statusDescriptor}): ${responseBody}`;
|
|
1702
|
+
throw new Error(message);
|
|
1703
|
+
}
|
|
1704
|
+
async maybeHandleUnauthorizedResponse(response) {
|
|
1705
|
+
if (response.status !== 401 || this.oauthProvider === undefined) {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
const discoveryClient = this.oauthMetadataDiscovery;
|
|
1709
|
+
if (discoveryClient === undefined) {
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
const challenge = parseBearerWwwAuthenticateHeader(response.headers.get("WWW-Authenticate"));
|
|
1713
|
+
const resourceMetadataUrl = challenge?.params.resource_metadata;
|
|
1714
|
+
const discovery = await discoveryClient.discover(this.url, {
|
|
1715
|
+
resourceMetadataUrl,
|
|
1716
|
+
});
|
|
1717
|
+
const result = await this.oauthProvider.handleUnauthorized({
|
|
1718
|
+
requestUrl: new URL(this.url),
|
|
1719
|
+
response: response.clone(),
|
|
1720
|
+
challenge,
|
|
1721
|
+
discovery,
|
|
1722
|
+
fetch: this.fetchImpl,
|
|
1723
|
+
});
|
|
1724
|
+
if (result.action === "retry") {
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
if (result.error !== undefined) {
|
|
1728
|
+
throw result.error;
|
|
1729
|
+
}
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
async forwardResponseMessages(response) {
|
|
1733
|
+
if (response.status === 202) {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
const contentType = response.headers.get("Content-Type");
|
|
1737
|
+
if (contentType === null) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
const normalizedContentType = contentType.toLowerCase();
|
|
1741
|
+
if (normalizedContentType.includes("text/event-stream")) {
|
|
1742
|
+
await this.forwardSseResponseMessages(response);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (normalizedContentType.includes("application/json")) {
|
|
1746
|
+
await this.forwardJsonResponseMessage(response);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async forwardSseResponseMessages(response) {
|
|
1750
|
+
if (response.body === null) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const parser = new SseParser();
|
|
1754
|
+
const decoder = new TextDecoder();
|
|
1755
|
+
const reader = response.body.getReader();
|
|
1756
|
+
this.openSseReaders.add(reader);
|
|
1757
|
+
try {
|
|
1758
|
+
while (true) {
|
|
1759
|
+
const { done, value } = await reader.read();
|
|
1760
|
+
if (done) {
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
if (value === undefined) {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
const messages = parser.push(decoder.decode(value, { stream: true }));
|
|
1767
|
+
this.writeSseMessages(messages);
|
|
1768
|
+
this.lastEventId = parser.lastEventId;
|
|
1769
|
+
}
|
|
1770
|
+
const trailingChunk = decoder.decode();
|
|
1771
|
+
if (trailingChunk.length > 0) {
|
|
1772
|
+
this.writeSseMessages(parser.push(trailingChunk));
|
|
1773
|
+
this.lastEventId = parser.lastEventId;
|
|
1774
|
+
}
|
|
1775
|
+
this.writeSseMessages(parser.flush());
|
|
1776
|
+
this.lastEventId = parser.lastEventId;
|
|
1777
|
+
}
|
|
1778
|
+
finally {
|
|
1779
|
+
this.openSseReaders.delete(reader);
|
|
1780
|
+
reader.releaseLock();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async forwardJsonResponseMessage(response) {
|
|
1784
|
+
const payload = await response.text();
|
|
1785
|
+
if (payload.length === 0) {
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const parsedPayload = JSON.parse(payload);
|
|
1789
|
+
this.writeReadableLine(JSON.stringify(parsedPayload));
|
|
1790
|
+
}
|
|
1791
|
+
writeSseMessages(messages) {
|
|
1792
|
+
for (const message of messages) {
|
|
1793
|
+
this.writeReadableLine(message.data);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
writeReadableLine(line) {
|
|
1797
|
+
if (this.disposed || this.readStream.destroyed || this.readStream.writableEnded) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
this.readStream.write(`${line}\n`);
|
|
1801
|
+
}
|
|
1802
|
+
async fetchWithOAuthRetry(input) {
|
|
1803
|
+
const request = async () => this.fetchWithAbort(this.url, {
|
|
1804
|
+
method: input.method,
|
|
1805
|
+
headers: await input.createHeaders(),
|
|
1806
|
+
body: input.body,
|
|
1807
|
+
});
|
|
1808
|
+
let response = await request();
|
|
1809
|
+
if (await this.maybeHandleUnauthorizedResponse(response)) {
|
|
1810
|
+
response = await request();
|
|
1811
|
+
}
|
|
1812
|
+
const oauthError = this.oauthProvider === undefined
|
|
1813
|
+
? null
|
|
1814
|
+
: this.readOAuthChallengeError(response);
|
|
1815
|
+
if (oauthError !== null) {
|
|
1816
|
+
throw oauthError;
|
|
1817
|
+
}
|
|
1818
|
+
return response;
|
|
1819
|
+
}
|
|
1820
|
+
readOAuthChallengeError(response) {
|
|
1821
|
+
if (response.status !== 401 && response.status !== 403) {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
const challenge = parseBearerWwwAuthenticateHeader(response.headers.get("WWW-Authenticate"));
|
|
1825
|
+
const error = challenge?.params.error;
|
|
1826
|
+
if (error === undefined || error.length === 0) {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
return new OAuthError({
|
|
1830
|
+
error,
|
|
1831
|
+
error_description: challenge?.params.error_description,
|
|
1832
|
+
error_uri: challenge?.params.error_uri,
|
|
1833
|
+
}, response.status);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
class HttpTransportGetSseNotSupportedError extends Error {
|
|
1837
|
+
constructor() {
|
|
1838
|
+
super("HTTP transport server does not support GET SSE streams");
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
export class McpError extends Error {
|
|
1842
|
+
code;
|
|
1843
|
+
constructor(code, message, data) {
|
|
1844
|
+
super(message);
|
|
1845
|
+
this.name = "McpError";
|
|
1846
|
+
this.code = code;
|
|
1847
|
+
if (data !== undefined) {
|
|
1848
|
+
this.data = data;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
export function serializeJsonRpcMessage(message) {
|
|
1853
|
+
return `${JSON.stringify(message)}\n`;
|
|
1854
|
+
}
|
|
1855
|
+
function chunkToString(chunk) {
|
|
1856
|
+
if (typeof chunk === "string") {
|
|
1857
|
+
return chunk;
|
|
1858
|
+
}
|
|
1859
|
+
if (chunk instanceof Uint8Array) {
|
|
1860
|
+
return Buffer.from(chunk).toString("utf8");
|
|
1861
|
+
}
|
|
1862
|
+
return String(chunk);
|
|
1863
|
+
}
|
|
1864
|
+
function normalizeLine(line) {
|
|
1865
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1866
|
+
}
|
|
1867
|
+
export async function* readLines(stream) {
|
|
1868
|
+
let buffer = "";
|
|
1869
|
+
for await (const chunk of stream) {
|
|
1870
|
+
buffer += chunkToString(chunk);
|
|
1871
|
+
while (true) {
|
|
1872
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
1873
|
+
if (newlineIndex === -1) {
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
const line = buffer.slice(0, newlineIndex);
|
|
1877
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
1878
|
+
yield normalizeLine(line);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (buffer.length > 0) {
|
|
1882
|
+
yield normalizeLine(buffer);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
export class SseParser {
|
|
1886
|
+
buffer = "";
|
|
1887
|
+
eventType;
|
|
1888
|
+
dataLines = [];
|
|
1889
|
+
eventId = "";
|
|
1890
|
+
hasEventId = false;
|
|
1891
|
+
_lastEventId;
|
|
1892
|
+
get lastEventId() {
|
|
1893
|
+
return this._lastEventId;
|
|
1894
|
+
}
|
|
1895
|
+
push(chunk) {
|
|
1896
|
+
if (chunk.length === 0) {
|
|
1897
|
+
return [];
|
|
1898
|
+
}
|
|
1899
|
+
this.buffer += chunk;
|
|
1900
|
+
const messages = [];
|
|
1901
|
+
while (true) {
|
|
1902
|
+
const newlineIndex = this.buffer.indexOf("\n");
|
|
1903
|
+
if (newlineIndex === -1) {
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
const line = normalizeLine(this.buffer.slice(0, newlineIndex));
|
|
1907
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
1908
|
+
this.consumeLine(line, messages);
|
|
1909
|
+
}
|
|
1910
|
+
return messages;
|
|
1911
|
+
}
|
|
1912
|
+
flush() {
|
|
1913
|
+
const messages = [];
|
|
1914
|
+
if (this.buffer.length > 0) {
|
|
1915
|
+
this.consumeLine(normalizeLine(this.buffer), messages);
|
|
1916
|
+
this.buffer = "";
|
|
1917
|
+
}
|
|
1918
|
+
this.emitEvent(messages);
|
|
1919
|
+
return messages;
|
|
1920
|
+
}
|
|
1921
|
+
consumeLine(line, messages) {
|
|
1922
|
+
if (line.length === 0) {
|
|
1923
|
+
this.emitEvent(messages);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (line.startsWith(":")) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const separatorIndex = line.indexOf(":");
|
|
1930
|
+
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
1931
|
+
const rawValue = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
1932
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
1933
|
+
if (field === "event") {
|
|
1934
|
+
this.eventType = value;
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (field === "data") {
|
|
1938
|
+
this.dataLines.push(value);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (field === "id") {
|
|
1942
|
+
if (value.includes("\0")) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
this.eventId = value;
|
|
1946
|
+
this.hasEventId = true;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
emitEvent(messages) {
|
|
1950
|
+
const eventType = this.eventType ?? "message";
|
|
1951
|
+
if (this.hasEventId) {
|
|
1952
|
+
this._lastEventId = this.eventId;
|
|
1953
|
+
}
|
|
1954
|
+
if (this.dataLines.length === 0 || eventType !== "message") {
|
|
1955
|
+
this.resetEvent();
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
const message = {
|
|
1959
|
+
data: this.dataLines.join("\n"),
|
|
1960
|
+
};
|
|
1961
|
+
if (this.hasEventId) {
|
|
1962
|
+
message.id = this.eventId;
|
|
1963
|
+
}
|
|
1964
|
+
messages.push(message);
|
|
1965
|
+
this.resetEvent();
|
|
1966
|
+
}
|
|
1967
|
+
resetEvent() {
|
|
1968
|
+
this.eventType = undefined;
|
|
1969
|
+
this.dataLines = [];
|
|
1970
|
+
this.eventId = "";
|
|
1971
|
+
this.hasEventId = false;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
export class JsonRpcMessageLayer {
|
|
1975
|
+
requestTimeoutMs;
|
|
1976
|
+
input;
|
|
1977
|
+
output;
|
|
1978
|
+
inputClosedReason;
|
|
1979
|
+
nextRequestId = 1;
|
|
1980
|
+
disposedError;
|
|
1981
|
+
pendingRequests = new Map();
|
|
1982
|
+
activeIncomingRequests = new Map();
|
|
1983
|
+
requestHandlers = new Map();
|
|
1984
|
+
notificationHandlers = new Map();
|
|
1985
|
+
constructor(input, output, requestTimeoutMs = 30_000, inputClosedReason) {
|
|
1986
|
+
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs < 0) {
|
|
1987
|
+
throw new Error("requestTimeoutMs must be a non-negative finite number");
|
|
1988
|
+
}
|
|
1989
|
+
this.input = input;
|
|
1990
|
+
this.output = output;
|
|
1991
|
+
this.inputClosedReason = inputClosedReason;
|
|
1992
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
1993
|
+
this.consumeInput().catch(() => undefined);
|
|
1994
|
+
}
|
|
1995
|
+
sendNotification(method, params) {
|
|
1996
|
+
if (this.disposedError !== undefined) {
|
|
1997
|
+
throw this.disposedError;
|
|
1998
|
+
}
|
|
1999
|
+
const message = {
|
|
2000
|
+
jsonrpc: "2.0",
|
|
2001
|
+
method,
|
|
2002
|
+
};
|
|
2003
|
+
if (params !== undefined) {
|
|
2004
|
+
message.params = params;
|
|
2005
|
+
}
|
|
2006
|
+
this.output.write(serializeJsonRpcMessage(message));
|
|
2007
|
+
}
|
|
2008
|
+
onRequest(method, handler) {
|
|
2009
|
+
this.requestHandlers.set(method, handler);
|
|
2010
|
+
}
|
|
2011
|
+
onNotification(method, handler) {
|
|
2012
|
+
this.notificationHandlers.set(method, handler);
|
|
2013
|
+
}
|
|
2014
|
+
sendRequest(method, params, options = {}) {
|
|
2015
|
+
if (this.disposedError !== undefined) {
|
|
2016
|
+
throw this.disposedError;
|
|
2017
|
+
}
|
|
2018
|
+
const id = this.nextRequestId;
|
|
2019
|
+
this.nextRequestId += 1;
|
|
2020
|
+
const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
|
|
2021
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
2022
|
+
throw new Error("timeoutMs must be a non-negative finite number");
|
|
2023
|
+
}
|
|
2024
|
+
if (options.onRequestId !== undefined) {
|
|
2025
|
+
options.onRequestId(id);
|
|
2026
|
+
}
|
|
2027
|
+
const message = {
|
|
2028
|
+
jsonrpc: "2.0",
|
|
2029
|
+
id,
|
|
2030
|
+
method,
|
|
2031
|
+
};
|
|
2032
|
+
if (params !== undefined) {
|
|
2033
|
+
message.params = params;
|
|
2034
|
+
}
|
|
2035
|
+
return new Promise((resolve, reject) => {
|
|
2036
|
+
const timeout = setTimeout(() => {
|
|
2037
|
+
this.pendingRequests.delete(id);
|
|
2038
|
+
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
2039
|
+
}, timeoutMs);
|
|
2040
|
+
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
2041
|
+
try {
|
|
2042
|
+
this.output.write(serializeJsonRpcMessage(message));
|
|
2043
|
+
}
|
|
2044
|
+
catch (error) {
|
|
2045
|
+
clearTimeout(timeout);
|
|
2046
|
+
this.pendingRequests.delete(id);
|
|
2047
|
+
reject(error);
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
dispose(reason = new Error("JSON-RPC message layer disposed")) {
|
|
2052
|
+
if (this.disposedError !== undefined) {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
this.disposedError = reason;
|
|
2056
|
+
for (const pending of this.pendingRequests.values()) {
|
|
2057
|
+
clearTimeout(pending.timeout);
|
|
2058
|
+
pending.reject(reason);
|
|
2059
|
+
}
|
|
2060
|
+
this.pendingRequests.clear();
|
|
2061
|
+
this.activeIncomingRequests.clear();
|
|
2062
|
+
}
|
|
2063
|
+
async consumeInput() {
|
|
2064
|
+
try {
|
|
2065
|
+
for await (const line of readLines(this.input)) {
|
|
2066
|
+
if (this.disposedError !== undefined) {
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
if (line.length === 0) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
let parsedLine;
|
|
2073
|
+
try {
|
|
2074
|
+
parsedLine = JSON.parse(line);
|
|
2075
|
+
}
|
|
2076
|
+
catch {
|
|
2077
|
+
await this.processParsedMessage({
|
|
2078
|
+
type: "invalid",
|
|
2079
|
+
id: null,
|
|
2080
|
+
error: parseError(),
|
|
2081
|
+
});
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (Array.isArray(parsedLine)) {
|
|
2085
|
+
for (const message of parsedLine) {
|
|
2086
|
+
if (this.disposedError !== undefined) {
|
|
2087
|
+
break;
|
|
2088
|
+
}
|
|
2089
|
+
await this.processParsedMessage(parseJsonRpcPayload(message));
|
|
2090
|
+
}
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
await this.processParsedMessage(parseJsonRpcPayload(parsedLine));
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
catch (error) {
|
|
2097
|
+
if (this.disposedError === undefined) {
|
|
2098
|
+
this.dispose(error instanceof Error
|
|
2099
|
+
? error
|
|
2100
|
+
: new Error(`JSON-RPC input stream failed: ${String(error)}`));
|
|
2101
|
+
}
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (this.disposedError === undefined) {
|
|
2105
|
+
const streamClosedReason = await this.resolveInputStreamClosedReason();
|
|
2106
|
+
if (this.disposedError === undefined) {
|
|
2107
|
+
this.dispose(streamClosedReason);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async resolveInputStreamClosedReason() {
|
|
2112
|
+
const streamClosedError = new Error("JSON-RPC input stream closed");
|
|
2113
|
+
if (this.inputClosedReason === undefined) {
|
|
2114
|
+
return streamClosedError;
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
return await Promise.race([
|
|
2118
|
+
this.inputClosedReason,
|
|
2119
|
+
new Promise((resolve) => {
|
|
2120
|
+
setTimeout(() => {
|
|
2121
|
+
resolve(streamClosedError);
|
|
2122
|
+
}, 50);
|
|
2123
|
+
}),
|
|
2124
|
+
]);
|
|
2125
|
+
}
|
|
2126
|
+
catch {
|
|
2127
|
+
return streamClosedError;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async processParsedMessage(parsed) {
|
|
2131
|
+
if (parsed.type === "request") {
|
|
2132
|
+
const handler = this.requestHandlers.get(parsed.message.method);
|
|
2133
|
+
if (handler === undefined) {
|
|
2134
|
+
this.output.write(serializeJsonRpcMessage({
|
|
2135
|
+
jsonrpc: "2.0",
|
|
2136
|
+
id: parsed.message.id,
|
|
2137
|
+
error: {
|
|
2138
|
+
code: ERROR_METHOD_NOT_FOUND,
|
|
2139
|
+
message: `Method not found: ${parsed.message.method}`,
|
|
2140
|
+
},
|
|
2141
|
+
}));
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
this.handleIncomingRequest(parsed.message, handler);
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
if (parsed.type === "notification") {
|
|
2148
|
+
if (parsed.message.method === "notifications/cancelled") {
|
|
2149
|
+
this.handleCancellationNotification(parsed.message.params);
|
|
2150
|
+
}
|
|
2151
|
+
const handler = this.notificationHandlers.get(parsed.message.method);
|
|
2152
|
+
if (handler === undefined) {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
try {
|
|
2156
|
+
await handler(parsed.message.params, {
|
|
2157
|
+
method: parsed.message.method,
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
catch {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
if (parsed.type === "invalid") {
|
|
2166
|
+
const errorResponse = {
|
|
2167
|
+
jsonrpc: "2.0",
|
|
2168
|
+
id: parsed.id,
|
|
2169
|
+
error: {
|
|
2170
|
+
code: parsed.error.code,
|
|
2171
|
+
message: parsed.error.message,
|
|
2172
|
+
},
|
|
2173
|
+
};
|
|
2174
|
+
if (parsed.error.data !== undefined) {
|
|
2175
|
+
errorResponse.error.data = parsed.error.data;
|
|
2176
|
+
}
|
|
2177
|
+
this.output.write(`${JSON.stringify(errorResponse)}\n`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (parsed.type !== "response") {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
const pending = this.pendingRequests.get(parsed.message.id);
|
|
2184
|
+
if (pending === undefined) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
this.pendingRequests.delete(parsed.message.id);
|
|
2188
|
+
clearTimeout(pending.timeout);
|
|
2189
|
+
if ("result" in parsed.message) {
|
|
2190
|
+
pending.resolve(parsed.message.result);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
pending.reject(new McpError(parsed.message.error.code, parsed.message.error.message, parsed.message.error.data));
|
|
2194
|
+
}
|
|
2195
|
+
handleIncomingRequest(message, handler) {
|
|
2196
|
+
const activeRequest = {
|
|
2197
|
+
cancelled: false,
|
|
2198
|
+
};
|
|
2199
|
+
this.activeIncomingRequests.set(message.id, activeRequest);
|
|
2200
|
+
void (async () => {
|
|
2201
|
+
try {
|
|
2202
|
+
const result = await handler(message.params, {
|
|
2203
|
+
id: message.id,
|
|
2204
|
+
method: message.method,
|
|
2205
|
+
});
|
|
2206
|
+
if (this.disposedError !== undefined || activeRequest.cancelled) {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
this.output.write(serializeJsonRpcMessage({
|
|
2210
|
+
jsonrpc: "2.0",
|
|
2211
|
+
id: message.id,
|
|
2212
|
+
result,
|
|
2213
|
+
}));
|
|
2214
|
+
}
|
|
2215
|
+
catch (error) {
|
|
2216
|
+
if (this.disposedError !== undefined || activeRequest.cancelled) {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2220
|
+
this.output.write(serializeJsonRpcMessage({
|
|
2221
|
+
jsonrpc: "2.0",
|
|
2222
|
+
id: message.id,
|
|
2223
|
+
error: {
|
|
2224
|
+
code: ERROR_INTERNAL,
|
|
2225
|
+
message: errorMessage,
|
|
2226
|
+
},
|
|
2227
|
+
}));
|
|
2228
|
+
}
|
|
2229
|
+
finally {
|
|
2230
|
+
const inFlightRequest = this.activeIncomingRequests.get(message.id);
|
|
2231
|
+
if (inFlightRequest === activeRequest) {
|
|
2232
|
+
this.activeIncomingRequests.delete(message.id);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
})();
|
|
2236
|
+
}
|
|
2237
|
+
handleCancellationNotification(params) {
|
|
2238
|
+
if (!isObjectRecord(params)) {
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
const requestId = params.requestId;
|
|
2242
|
+
if (!isRequestId(requestId)) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const activeRequest = this.activeIncomingRequests.get(requestId);
|
|
2246
|
+
if (activeRequest === undefined) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
activeRequest.cancelled = true;
|
|
2250
|
+
this.activeIncomingRequests.delete(requestId);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function isObjectRecord(value) {
|
|
2254
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2255
|
+
}
|
|
2256
|
+
function hasOwn(value, property) {
|
|
2257
|
+
return Object.prototype.hasOwnProperty.call(value, property);
|
|
2258
|
+
}
|
|
2259
|
+
function isRequestId(value) {
|
|
2260
|
+
return typeof value === "string" || typeof value === "number";
|
|
2261
|
+
}
|
|
2262
|
+
function isLogLevel(value) {
|
|
2263
|
+
return (value === "debug" ||
|
|
2264
|
+
value === "info" ||
|
|
2265
|
+
value === "notice" ||
|
|
2266
|
+
value === "warning" ||
|
|
2267
|
+
value === "error" ||
|
|
2268
|
+
value === "critical" ||
|
|
2269
|
+
value === "alert" ||
|
|
2270
|
+
value === "emergency");
|
|
2271
|
+
}
|
|
2272
|
+
function toRequestId(value) {
|
|
2273
|
+
return isRequestId(value) ? value : null;
|
|
2274
|
+
}
|
|
2275
|
+
function parseError() {
|
|
2276
|
+
return new McpError(ERROR_PARSE, "Parse error");
|
|
2277
|
+
}
|
|
2278
|
+
function invalidRequest() {
|
|
2279
|
+
return new McpError(ERROR_INVALID_REQUEST, "Invalid Request");
|
|
2280
|
+
}
|
|
2281
|
+
function isJsonRpcErrorObject(value) {
|
|
2282
|
+
if (!isObjectRecord(value)) {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
if (typeof value.code !== "number" || typeof value.message !== "string") {
|
|
2286
|
+
return false;
|
|
2287
|
+
}
|
|
2288
|
+
return value.data === undefined || hasOwn(value, "data");
|
|
2289
|
+
}
|
|
2290
|
+
export function parseJsonRpcMessage(line) {
|
|
2291
|
+
let parsed;
|
|
2292
|
+
try {
|
|
2293
|
+
parsed = JSON.parse(line);
|
|
2294
|
+
}
|
|
2295
|
+
catch {
|
|
2296
|
+
return {
|
|
2297
|
+
type: "invalid",
|
|
2298
|
+
id: null,
|
|
2299
|
+
error: parseError(),
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return parseJsonRpcPayload(parsed);
|
|
2303
|
+
}
|
|
2304
|
+
function parseJsonRpcPayload(parsed) {
|
|
2305
|
+
if (!isObjectRecord(parsed)) {
|
|
2306
|
+
return {
|
|
2307
|
+
type: "invalid",
|
|
2308
|
+
id: null,
|
|
2309
|
+
error: invalidRequest(),
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
const id = toRequestId(parsed.id);
|
|
2313
|
+
if (parsed.jsonrpc !== "2.0") {
|
|
2314
|
+
return {
|
|
2315
|
+
type: "invalid",
|
|
2316
|
+
id,
|
|
2317
|
+
error: invalidRequest(),
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
const hasMethod = hasOwn(parsed, "method");
|
|
2321
|
+
const hasId = hasOwn(parsed, "id");
|
|
2322
|
+
if (hasMethod) {
|
|
2323
|
+
if (typeof parsed.method !== "string") {
|
|
2324
|
+
return {
|
|
2325
|
+
type: "invalid",
|
|
2326
|
+
id,
|
|
2327
|
+
error: invalidRequest(),
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
if (hasId) {
|
|
2331
|
+
if (!isRequestId(parsed.id)) {
|
|
2332
|
+
return {
|
|
2333
|
+
type: "invalid",
|
|
2334
|
+
id: null,
|
|
2335
|
+
error: invalidRequest(),
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
const request = {
|
|
2339
|
+
jsonrpc: "2.0",
|
|
2340
|
+
id: parsed.id,
|
|
2341
|
+
method: parsed.method,
|
|
2342
|
+
};
|
|
2343
|
+
if (hasOwn(parsed, "params")) {
|
|
2344
|
+
request.params = parsed.params;
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
type: "request",
|
|
2348
|
+
message: request,
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
const notification = {
|
|
2352
|
+
jsonrpc: "2.0",
|
|
2353
|
+
method: parsed.method,
|
|
2354
|
+
};
|
|
2355
|
+
if (hasOwn(parsed, "params")) {
|
|
2356
|
+
notification.params = parsed.params;
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
type: "notification",
|
|
2360
|
+
message: notification,
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
if (!hasId || !isRequestId(parsed.id)) {
|
|
2364
|
+
return {
|
|
2365
|
+
type: "invalid",
|
|
2366
|
+
id,
|
|
2367
|
+
error: invalidRequest(),
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const hasResult = hasOwn(parsed, "result");
|
|
2371
|
+
const hasError = hasOwn(parsed, "error");
|
|
2372
|
+
if (hasResult === hasError) {
|
|
2373
|
+
return {
|
|
2374
|
+
type: "invalid",
|
|
2375
|
+
id: parsed.id,
|
|
2376
|
+
error: invalidRequest(),
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
if (hasResult) {
|
|
2380
|
+
return {
|
|
2381
|
+
type: "response",
|
|
2382
|
+
message: {
|
|
2383
|
+
jsonrpc: "2.0",
|
|
2384
|
+
id: parsed.id,
|
|
2385
|
+
result: parsed.result,
|
|
2386
|
+
},
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
if (!isJsonRpcErrorObject(parsed.error)) {
|
|
2390
|
+
return {
|
|
2391
|
+
type: "invalid",
|
|
2392
|
+
id: parsed.id,
|
|
2393
|
+
error: invalidRequest(),
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
return {
|
|
2397
|
+
type: "response",
|
|
2398
|
+
message: {
|
|
2399
|
+
jsonrpc: "2.0",
|
|
2400
|
+
id: parsed.id,
|
|
2401
|
+
error: parsed.error,
|
|
2402
|
+
},
|
|
2403
|
+
};
|
|
2404
|
+
}
|