@plurnk/plurnk-mcp 1.6.1 → 1.8.0
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/.env.defaults +31 -4
- package/README.md +161 -42
- package/SPEC.md +486 -28
- package/dist/McpExecutor.d.ts +28 -5
- package/dist/McpExecutor.d.ts.map +1 -1
- package/dist/McpExecutor.js +144 -32
- package/dist/McpExecutor.js.map +1 -1
- package/dist/McpResources.d.ts +2 -2
- package/dist/McpResources.d.ts.map +1 -1
- package/dist/McpResources.js +114 -24
- package/dist/McpResources.js.map +1 -1
- package/dist/Module.d.ts +30 -5
- package/dist/Module.d.ts.map +1 -1
- package/dist/Module.js +857 -36
- package/dist/Module.js.map +1 -1
- package/dist/ToolPresentation.d.ts +7 -0
- package/dist/ToolPresentation.d.ts.map +1 -0
- package/dist/ToolPresentation.js +224 -0
- package/dist/ToolPresentation.js.map +1 -0
- package/dist/capabilityMatrix.d.ts +23 -0
- package/dist/capabilityMatrix.d.ts.map +1 -0
- package/dist/capabilityMatrix.js +403 -0
- package/dist/capabilityMatrix.js.map +1 -0
- package/dist/client.d.ts +27 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +537 -91
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +14 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +257 -51
- package/dist/config.js.map +1 -1
- package/dist/extensionChannel.d.ts +25 -0
- package/dist/extensionChannel.d.ts.map +1 -0
- package/dist/extensionChannel.js +195 -0
- package/dist/extensionChannel.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inputRequired.d.ts +36 -0
- package/dist/inputRequired.d.ts.map +1 -0
- package/dist/inputRequired.js +171 -0
- package/dist/inputRequired.js.map +1 -0
- package/dist/mcp-watchdog.mjs +106 -0
- package/dist/oauth.d.ts +28 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +149 -0
- package/dist/oauth.js.map +1 -0
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +8 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocolHeaders.d.ts +4 -0
- package/dist/protocolHeaders.d.ts.map +1 -0
- package/dist/protocolHeaders.js +87 -0
- package/dist/protocolHeaders.js.map +1 -0
- package/dist/subscriptions.d.ts +15 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +188 -0
- package/dist/subscriptions.js.map +1 -0
- package/dist/tasks.d.ts +19 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +334 -0
- package/dist/tasks.js.map +1 -0
- package/package.json +12 -7
package/dist/client.js
CHANGED
|
@@ -1,71 +1,363 @@
|
|
|
1
|
-
import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
|
|
1
|
+
import { OAuthClientFlowError, OAuthError, UnauthorizedError, Client, ClientCredentialsProvider, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
|
|
2
2
|
import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/client/stdio";
|
|
3
|
+
import { Validator, } from "@plurnk/plurnk-contracts";
|
|
3
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
-
import { connectTimeoutMs, requestTimeoutMs, } from "./config.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
import { connectTimeoutMs, expandReferences, requestTimeoutMs, } from "./config.js";
|
|
6
|
+
import { INPUT_REQUIRED_MAX_ROUNDS, runInputRequiredRequest, } from "./inputRequired.js";
|
|
7
|
+
import InteractiveOAuthProvider from "./oauth.js";
|
|
8
|
+
import ExtensionChannel from "./extensionChannel.js";
|
|
9
|
+
import { mcpRoutingHeaderValue } from "./protocolHeaders.js";
|
|
10
|
+
import { MCP_OAUTH_CLIENT_CREDENTIALS_EXTENSION_ID, MCP_PROTOCOL_VERSION, } from "./protocol.js";
|
|
11
|
+
import { staticClientCapabilities } from "./capabilityMatrix.js";
|
|
12
|
+
import Subscriptions from "./subscriptions.js";
|
|
13
|
+
import { callToolWithTasks, serverSupportsTasks, } from "./tasks.js";
|
|
14
|
+
// {§mcp-authority} — capability source: the modern discover result when the
|
|
15
|
+
// server offered it, else the legacy initialize result the SDK already folded.
|
|
16
|
+
const serverCapabilities = (client) => client.getDiscoverResult()?.capabilities
|
|
17
|
+
?? client.getServerCapabilities();
|
|
18
|
+
const message = (error) => error instanceof Error ? error.message : String(error);
|
|
19
|
+
const errorsOf = (error) => error instanceof AggregateError ? [...error.errors] : [error];
|
|
20
|
+
// {§oauth-client-credentials} — a rejected client-credentials grant is an
|
|
21
|
+
// authorization fact, never a generic unavailability.
|
|
22
|
+
export const isClientCredentialsRejection = (error) => errorsOf(error).some((candidate) => {
|
|
23
|
+
let current = candidate;
|
|
24
|
+
while (current !== undefined && current !== null) {
|
|
25
|
+
if (OAuthError.isInstance(current) ||
|
|
26
|
+
UnauthorizedError.isInstance(current) ||
|
|
27
|
+
OAuthClientFlowError.isInstance(current)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
const expandedRecord = (source, environ, field) => source === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: Object.fromEntries(Object.entries(source).map(([key, value]) => [
|
|
37
|
+
key,
|
|
38
|
+
expandReferences(value, environ, `${field}.${key}`),
|
|
39
|
+
]));
|
|
40
|
+
const requireString = (value, field) => {
|
|
41
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
42
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
const resolveDefinition = (source, environ) => {
|
|
47
|
+
const definition = Validator.assertMcpServerDefinition(source);
|
|
48
|
+
if (definition.transport === "stdio") {
|
|
49
|
+
return {
|
|
50
|
+
transport: "stdio",
|
|
51
|
+
command: expandReferences(requireString(definition.command, `${definition.name}.command`), environ, `${definition.name}.command`),
|
|
52
|
+
args: (definition.args ?? []).map((argument, index) => expandReferences(argument, environ, `${definition.name}.args[${index}]`)),
|
|
53
|
+
...(definition.cwd === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { cwd: expandReferences(definition.cwd, environ, `${definition.name}.cwd`) }),
|
|
56
|
+
...(definition.env === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { env: expandedRecord(definition.env, environ, `${definition.name}.env`) }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const url = requireString(definition.url, `${definition.name}.url`);
|
|
62
|
+
const headers = expandedRecord(definition.headers, environ, `${definition.name}.headers`);
|
|
63
|
+
const authorizationHeader = Object.keys(headers ?? {}).find((name) => name.toLowerCase() === "authorization");
|
|
64
|
+
if (definition.authorization !== undefined && authorizationHeader !== undefined) {
|
|
65
|
+
throw new Error(`${definition.name}.authorization conflicts with the Authorization header.`);
|
|
66
|
+
}
|
|
67
|
+
if (definition.authorization === undefined) {
|
|
68
|
+
return {
|
|
69
|
+
transport: "http",
|
|
70
|
+
url,
|
|
71
|
+
...(headers === undefined ? {} : { headers }),
|
|
72
|
+
cachePartition: "anonymous",
|
|
73
|
+
};
|
|
11
74
|
}
|
|
75
|
+
if (definition.authorization.type === "bearer") {
|
|
76
|
+
const token = expandReferences(definition.authorization.token, environ, `${definition.name}.authorization.token`);
|
|
77
|
+
if (token.length === 0)
|
|
78
|
+
throw new Error(`${definition.name}.authorization.token resolved empty.`);
|
|
79
|
+
return {
|
|
80
|
+
transport: "http",
|
|
81
|
+
url,
|
|
82
|
+
...(headers === undefined ? {} : { headers }),
|
|
83
|
+
authProvider: { token: async () => token },
|
|
84
|
+
cachePartition: `bearer:${definition.authorization.token}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (definition.authorization.type === "client-credentials") {
|
|
88
|
+
const secret = expandReferences(definition.authorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`);
|
|
89
|
+
if (secret.length === 0) {
|
|
90
|
+
throw new Error(`${definition.name}.authorization.clientSecret resolved empty.`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
transport: "http",
|
|
94
|
+
url,
|
|
95
|
+
...(headers === undefined ? {} : { headers }),
|
|
96
|
+
clientCredentials: true,
|
|
97
|
+
authProvider: new ClientCredentialsProvider({
|
|
98
|
+
clientId: definition.authorization.clientId,
|
|
99
|
+
clientSecret: secret,
|
|
100
|
+
// {§oauth-client-credentials} — a declared issuer binds the static
|
|
101
|
+
// credential to that authorization server (SEP-2352); absent, the
|
|
102
|
+
// SDK's legacy no-binding behaviour applies.
|
|
103
|
+
...(definition.authorization.issuer === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { expectedIssuer: definition.authorization.issuer }),
|
|
106
|
+
...(definition.authorization.scope === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { scope: definition.authorization.scope }),
|
|
109
|
+
}),
|
|
110
|
+
cachePartition: `client:${definition.authorization.clientId}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const oauthAuthorization = definition.authorization;
|
|
114
|
+
// {§oauth-lifetime} — registration data, tokens, PKCE verifier, and
|
|
115
|
+
// callback state remain process-memory in this provider; the durable
|
|
116
|
+
// definition stays unexpanded.
|
|
117
|
+
const oauthProvider = new InteractiveOAuthProvider({
|
|
118
|
+
redirectUrl: oauthAuthorization.redirectUrl,
|
|
119
|
+
...(oauthAuthorization.scope === undefined ? {} : { scope: oauthAuthorization.scope }),
|
|
120
|
+
...("clientMetadataUrl" in oauthAuthorization
|
|
121
|
+
? { clientMetadataUrl: oauthAuthorization.clientMetadataUrl }
|
|
122
|
+
: {}),
|
|
123
|
+
...("clientId" in oauthAuthorization
|
|
124
|
+
? {
|
|
125
|
+
clientId: oauthAuthorization.clientId,
|
|
126
|
+
clientSecret: expandReferences(oauthAuthorization.clientSecret, environ, `${definition.name}.authorization.clientSecret`),
|
|
127
|
+
}
|
|
128
|
+
: {}),
|
|
129
|
+
});
|
|
130
|
+
const cachePartition = "clientMetadataUrl" in oauthAuthorization
|
|
131
|
+
? `oauth:cimd:${oauthAuthorization.clientMetadataUrl}`
|
|
132
|
+
: "clientId" in oauthAuthorization
|
|
133
|
+
? `oauth:client:${oauthAuthorization.clientId}`
|
|
134
|
+
: "oauth:dynamic";
|
|
135
|
+
return {
|
|
136
|
+
transport: "http",
|
|
137
|
+
url,
|
|
138
|
+
...(headers === undefined ? {} : { headers }),
|
|
139
|
+
authProvider: oauthProvider,
|
|
140
|
+
oauthProvider,
|
|
141
|
+
cachePartition,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const openTransport = (definition) => {
|
|
145
|
+
if (definition.transport === "http") {
|
|
146
|
+
return new StreamableHTTPClientTransport(new URL(definition.url), {
|
|
147
|
+
...(definition.headers === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: { requestInit: { headers: definition.headers } }),
|
|
150
|
+
...(definition.authProvider === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { authProvider: definition.authProvider }),
|
|
153
|
+
fetch: async (url, init) => {
|
|
154
|
+
const body = typeof init?.body === "string"
|
|
155
|
+
? (() => {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(init.body);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
})()
|
|
163
|
+
: undefined;
|
|
164
|
+
const request = body !== null && typeof body === "object" && !Array.isArray(body)
|
|
165
|
+
? body
|
|
166
|
+
: undefined;
|
|
167
|
+
if (["tasks/get", "tasks/update", "tasks/cancel"].includes(String(request?.method))
|
|
168
|
+
&& typeof request?.params?.taskId === "string") {
|
|
169
|
+
const headers = new Headers(init?.headers);
|
|
170
|
+
headers.set("Mcp-Name", mcpRoutingHeaderValue(request.params.taskId));
|
|
171
|
+
return fetch(url, { ...init, headers });
|
|
172
|
+
}
|
|
173
|
+
return fetch(url, init);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// {§mcp-stdio-process-ownership} — stdio servers spawn through the
|
|
178
|
+
// parent-death watchdog wrapper: the real server runs detached (own
|
|
179
|
+
// process group) and the wrapper group-kills it (grandchildren included)
|
|
180
|
+
// when the daemon dies by ANY path, including SIGKILL where close()
|
|
181
|
+
// never runs. Protocol bytes are forwarded verbatim.
|
|
12
182
|
return new StdioClientTransport({
|
|
13
|
-
command:
|
|
14
|
-
args:
|
|
15
|
-
|
|
183
|
+
command: process.execPath,
|
|
184
|
+
args: [
|
|
185
|
+
new URL("./mcp-watchdog.mjs", import.meta.url).pathname,
|
|
186
|
+
String(process.pid),
|
|
187
|
+
"--",
|
|
188
|
+
definition.command,
|
|
189
|
+
...definition.args,
|
|
190
|
+
],
|
|
191
|
+
cwd: definition.cwd,
|
|
16
192
|
env: {
|
|
17
193
|
...getDefaultEnvironment(),
|
|
18
|
-
...
|
|
194
|
+
...definition.env,
|
|
19
195
|
},
|
|
20
196
|
});
|
|
21
197
|
};
|
|
22
|
-
|
|
23
|
-
|
|
198
|
+
export class AuthorizationRequiredError extends Error {
|
|
199
|
+
authorizationUrl;
|
|
200
|
+
constructor(authorizationUrl, cause) {
|
|
201
|
+
super("MCP server requires interactive OAuth authorization.", { cause });
|
|
202
|
+
this.name = "AuthorizationRequiredError";
|
|
203
|
+
this.authorizationUrl = authorizationUrl;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const openClient = async (definition, environ, options, transport) => {
|
|
207
|
+
const changed = (error) => options.onCatalogChanged?.(error ?? null);
|
|
208
|
+
const clientInfo = {
|
|
24
209
|
name: packageJson.name,
|
|
25
210
|
version: packageJson.version,
|
|
26
|
-
}
|
|
211
|
+
};
|
|
212
|
+
// {§mcp-capability-matrix} — static advertisement derives from the
|
|
213
|
+
// capability matrix by construction; the conditional client-credentials
|
|
214
|
+
// extension ({§oauth-client-credentials}) is added only on connections
|
|
215
|
+
// that actually hold a client-credentials definition.
|
|
216
|
+
const matrixCapabilities = staticClientCapabilities();
|
|
217
|
+
const clientCapabilities = {
|
|
218
|
+
...matrixCapabilities,
|
|
219
|
+
extensions: {
|
|
220
|
+
...matrixCapabilities.extensions,
|
|
221
|
+
...(definition.transport === "http" && definition.clientCredentials === true
|
|
222
|
+
? { [MCP_OAUTH_CLIENT_CREDENTIALS_EXTENSION_ID]: {} }
|
|
223
|
+
: {}),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
const client = new Client(clientInfo, {
|
|
227
|
+
capabilities: clientCapabilities,
|
|
27
228
|
versionNegotiation: {
|
|
28
|
-
mode:
|
|
229
|
+
mode: "auto",
|
|
29
230
|
},
|
|
30
231
|
inputRequired: {
|
|
31
232
|
autoFulfill: false,
|
|
233
|
+
maxRounds: INPUT_REQUIRED_MAX_ROUNDS,
|
|
234
|
+
},
|
|
235
|
+
listChanged: {
|
|
236
|
+
tools: {
|
|
237
|
+
autoRefresh: false,
|
|
238
|
+
onChanged: (error) => changed(error ?? undefined),
|
|
239
|
+
},
|
|
240
|
+
resources: {
|
|
241
|
+
autoRefresh: false,
|
|
242
|
+
onChanged: (error) => changed(error ?? undefined),
|
|
243
|
+
},
|
|
244
|
+
prompts: {
|
|
245
|
+
autoRefresh: false,
|
|
246
|
+
onChanged: (error) => changed(error ?? undefined),
|
|
247
|
+
},
|
|
32
248
|
},
|
|
249
|
+
...(definition.transport === "http"
|
|
250
|
+
? { cachePartition: definition.cachePartition }
|
|
251
|
+
: {}),
|
|
33
252
|
});
|
|
253
|
+
client.onerror = (error) => options.onInfrastructureError?.(error);
|
|
34
254
|
try {
|
|
35
|
-
await client.connect(
|
|
255
|
+
await client.connect(transport, {
|
|
36
256
|
timeout: connectTimeoutMs(environ),
|
|
37
257
|
});
|
|
38
258
|
}
|
|
39
259
|
catch (cause) {
|
|
260
|
+
const authorizationUrl = definition.transport === "http"
|
|
261
|
+
? definition.oauthProvider?.takeAuthorizationUrl()
|
|
262
|
+
: undefined;
|
|
263
|
+
let closeFailure;
|
|
40
264
|
try {
|
|
41
265
|
await client.close();
|
|
42
266
|
}
|
|
43
|
-
catch (
|
|
44
|
-
|
|
267
|
+
catch (error) {
|
|
268
|
+
closeFailure = error;
|
|
269
|
+
}
|
|
270
|
+
if (authorizationUrl !== undefined) {
|
|
271
|
+
throw new AuthorizationRequiredError(authorizationUrl.href, closeFailure === undefined ? cause : new AggregateError([cause, closeFailure]));
|
|
272
|
+
}
|
|
273
|
+
if (closeFailure !== undefined) {
|
|
274
|
+
throw new AggregateError([cause, closeFailure], `MCP ${MCP_PROTOCOL_VERSION} connection and cleanup failed.`);
|
|
45
275
|
}
|
|
46
276
|
throw new Error(`MCP ${MCP_PROTOCOL_VERSION} connection failed.`, { cause });
|
|
47
277
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
278
|
+
// {§mcp-authority} — negotiate-and-degrade. The pinned revision is the host's
|
|
279
|
+
// own wire authority: a modern pinned server with a discover result gets the
|
|
280
|
+
// complete extension wire. Anything the SDK negotiated below it is an
|
|
281
|
+
// ordinary MCP server — standard tool/resource/prompt surface from its
|
|
282
|
+
// initialize result, no discover identity, no tasks, no extension channel.
|
|
283
|
+
const era = client.getProtocolEra();
|
|
284
|
+
const negotiated = client.getNegotiatedProtocolVersion() ?? "";
|
|
285
|
+
const discover = client.getDiscoverResult();
|
|
286
|
+
const modern = era === "modern" && negotiated === MCP_PROTOCOL_VERSION && discover !== undefined;
|
|
287
|
+
const extensions = modern
|
|
288
|
+
? new ExtensionChannel(transport, {
|
|
289
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
290
|
+
clientInfo,
|
|
291
|
+
clientCapabilities,
|
|
292
|
+
cancelRequest: async (requestId) => client.notification({
|
|
293
|
+
method: "notifications/cancelled",
|
|
294
|
+
params: { requestId },
|
|
295
|
+
}),
|
|
296
|
+
onError: options.onInfrastructureError,
|
|
297
|
+
})
|
|
298
|
+
: null;
|
|
299
|
+
return {
|
|
300
|
+
client,
|
|
301
|
+
transport,
|
|
302
|
+
extensions,
|
|
303
|
+
protocolVersion: negotiated,
|
|
304
|
+
subscriptions: new Subscriptions(client, {
|
|
305
|
+
timeout: requestTimeoutMs(environ),
|
|
306
|
+
tasks: serverSupportsTasks(discover?.capabilities ?? client.getServerCapabilities()),
|
|
307
|
+
onError: options.onInfrastructureError,
|
|
308
|
+
}),
|
|
309
|
+
};
|
|
55
310
|
};
|
|
56
311
|
export default class ServerConnection {
|
|
57
|
-
#
|
|
312
|
+
#definition;
|
|
313
|
+
#resolved;
|
|
58
314
|
#environ;
|
|
315
|
+
#options;
|
|
59
316
|
#client;
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
317
|
+
#pendingAuthorization;
|
|
318
|
+
#activeRequests = 0;
|
|
319
|
+
#closed = false;
|
|
320
|
+
constructor(definition, environ = process.env, options = {}) {
|
|
321
|
+
this.#definition = structuredClone(Validator.assertMcpServerDefinition(definition));
|
|
322
|
+
this.#resolved = resolveDefinition(this.#definition, environ);
|
|
63
323
|
this.#environ = environ;
|
|
324
|
+
this.#options = options;
|
|
325
|
+
}
|
|
326
|
+
get definition() {
|
|
327
|
+
return structuredClone(this.#definition);
|
|
328
|
+
}
|
|
329
|
+
get activeRequests() {
|
|
330
|
+
return this.#activeRequests;
|
|
64
331
|
}
|
|
65
|
-
|
|
332
|
+
get authorizationUrl() {
|
|
333
|
+
return this.#pendingAuthorization?.authorizationUrl ?? null;
|
|
334
|
+
}
|
|
335
|
+
assertReplaceable() {
|
|
336
|
+
if (this.#activeRequests !== 0) {
|
|
337
|
+
throw new Error(`MCP server '${this.#definition.name}' has ${this.#activeRequests} active user request(s).`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async #open() {
|
|
341
|
+
if (this.#closed)
|
|
342
|
+
throw new Error(`MCP server '${this.#definition.name}' connection is closed.`);
|
|
343
|
+
if (this.#pendingAuthorization !== undefined) {
|
|
344
|
+
throw new AuthorizationRequiredError(this.#pendingAuthorization.authorizationUrl);
|
|
345
|
+
}
|
|
66
346
|
if (this.#client !== undefined)
|
|
67
347
|
return this.#client;
|
|
68
|
-
const
|
|
348
|
+
const transport = openTransport(this.#resolved);
|
|
349
|
+
const pending = openClient(this.#resolved, this.#environ, this.#options, transport).catch((cause) => {
|
|
350
|
+
if (cause instanceof AuthorizationRequiredError
|
|
351
|
+
&& this.#resolved.transport === "http"
|
|
352
|
+
&& this.#resolved.oauthProvider !== undefined
|
|
353
|
+
&& transport instanceof StreamableHTTPClientTransport) {
|
|
354
|
+
this.#pendingAuthorization = {
|
|
355
|
+
transport,
|
|
356
|
+
provider: this.#resolved.oauthProvider,
|
|
357
|
+
authorizationUrl: cause.authorizationUrl,
|
|
358
|
+
standaloneTransport: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
69
361
|
if (this.#client === pending)
|
|
70
362
|
this.#client = undefined;
|
|
71
363
|
throw cause;
|
|
@@ -73,81 +365,235 @@ export default class ServerConnection {
|
|
|
73
365
|
this.#client = pending;
|
|
74
366
|
return pending;
|
|
75
367
|
}
|
|
76
|
-
|
|
77
|
-
return this.#
|
|
368
|
+
async connect() {
|
|
369
|
+
return (await this.#open()).client;
|
|
78
370
|
}
|
|
79
|
-
async
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
371
|
+
async finishAuthorization(callbackUrl) {
|
|
372
|
+
const pending = this.#pendingAuthorization;
|
|
373
|
+
if (pending === undefined) {
|
|
374
|
+
throw new Error(`MCP server '${this.#definition.name}' has no pending OAuth authorization.`);
|
|
375
|
+
}
|
|
376
|
+
const callback = new URL(callbackUrl);
|
|
377
|
+
const expected = new URL(pending.provider.redirectUrl);
|
|
378
|
+
if (callback.protocol !== expected.protocol
|
|
379
|
+
|| callback.host !== expected.host
|
|
380
|
+
|| callback.pathname !== expected.pathname) {
|
|
381
|
+
throw new Error("OAuth callback URL does not match the configured redirect URL.");
|
|
382
|
+
}
|
|
383
|
+
pending.provider.assertCallbackState(callback);
|
|
384
|
+
await pending.transport.finishAuth(callback.searchParams);
|
|
385
|
+
this.#pendingAuthorization = undefined;
|
|
386
|
+
await this.connect();
|
|
84
387
|
}
|
|
85
|
-
async
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
388
|
+
async #request(run) {
|
|
389
|
+
this.#activeRequests += 1;
|
|
390
|
+
try {
|
|
391
|
+
const opened = await this.#open();
|
|
392
|
+
try {
|
|
393
|
+
return await run(opened.client, opened.subscriptions, opened.extensions);
|
|
394
|
+
}
|
|
395
|
+
catch (cause) {
|
|
396
|
+
const authorization = this.#takeAuthorization(opened, cause);
|
|
397
|
+
if (authorization !== null)
|
|
398
|
+
throw authorization;
|
|
399
|
+
throw cause;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
this.#activeRequests -= 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
#takeAuthorization(opened, cause) {
|
|
407
|
+
if (this.#resolved.transport !== "http"
|
|
408
|
+
|| this.#resolved.oauthProvider === undefined
|
|
409
|
+
|| !(opened.transport instanceof StreamableHTTPClientTransport)) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const authorizationUrl = this.#resolved.oauthProvider.takeAuthorizationUrl();
|
|
413
|
+
if (authorizationUrl === undefined)
|
|
414
|
+
return null;
|
|
415
|
+
this.#pendingAuthorization = {
|
|
416
|
+
transport: opened.transport,
|
|
417
|
+
provider: this.#resolved.oauthProvider,
|
|
418
|
+
authorizationUrl: authorizationUrl.href,
|
|
419
|
+
standaloneTransport: false,
|
|
112
420
|
};
|
|
421
|
+
return new AuthorizationRequiredError(authorizationUrl.href, cause);
|
|
113
422
|
}
|
|
114
|
-
async
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
423
|
+
async tools(signal) {
|
|
424
|
+
return this.#request(async (client) => {
|
|
425
|
+
if (serverCapabilities(client)?.tools === undefined)
|
|
426
|
+
return [];
|
|
427
|
+
const { tools } = await client.listTools(undefined, this.#requestOptions(signal));
|
|
428
|
+
return tools;
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async catalog(signal) {
|
|
432
|
+
return this.#request(async (client) => {
|
|
433
|
+
// {§mcp-authority} — the discover result is the modern identity and
|
|
434
|
+
// capability source; a legacy server's initialize result supplies the
|
|
435
|
+
// same facts at its negotiated revision.
|
|
436
|
+
const capabilities = serverCapabilities(client) ?? {};
|
|
437
|
+
const [tools, resources, resourceTemplates, prompts] = await Promise.all([
|
|
438
|
+
capabilities.tools === undefined
|
|
439
|
+
? Promise.resolve([])
|
|
440
|
+
: client.listTools(undefined, this.#requestOptions(signal)).then((result) => result.tools),
|
|
441
|
+
capabilities.resources === undefined
|
|
442
|
+
? Promise.resolve([])
|
|
443
|
+
: client.listResources(undefined, this.#requestOptions(signal)).then((result) => result.resources),
|
|
444
|
+
capabilities.resources === undefined
|
|
445
|
+
? Promise.resolve([])
|
|
446
|
+
: client.listResourceTemplates(undefined, this.#requestOptions(signal))
|
|
447
|
+
.then((result) => result.resourceTemplates),
|
|
448
|
+
capabilities.prompts === undefined
|
|
449
|
+
? Promise.resolve([])
|
|
450
|
+
: client.listPrompts(undefined, this.#requestOptions(signal)).then((result) => result.prompts),
|
|
451
|
+
]);
|
|
118
452
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
453
|
+
protocolVersion: client.getNegotiatedProtocolVersion() ?? "",
|
|
454
|
+
server: client.getServerVersion(),
|
|
455
|
+
instructions: client.getInstructions(),
|
|
456
|
+
capabilities,
|
|
457
|
+
tools,
|
|
458
|
+
resources,
|
|
459
|
+
resourceTemplates,
|
|
460
|
+
prompts,
|
|
121
461
|
};
|
|
122
|
-
}
|
|
123
|
-
const [resources, resourceTemplates] = await Promise.all([
|
|
124
|
-
client.listResources(undefined, this.#requestOptions(signal))
|
|
125
|
-
.then((result) => result.resources),
|
|
126
|
-
client.listResourceTemplates(undefined, this.#requestOptions(signal))
|
|
127
|
-
.then((result) => result.resourceTemplates),
|
|
128
|
-
]);
|
|
129
|
-
return {
|
|
130
|
-
resources,
|
|
131
|
-
resourceTemplates,
|
|
132
|
-
};
|
|
462
|
+
});
|
|
133
463
|
}
|
|
134
|
-
async
|
|
135
|
-
return (
|
|
464
|
+
async resources(signal) {
|
|
465
|
+
return this.#request(async (client) => {
|
|
466
|
+
if (serverCapabilities(client)?.resources === undefined) {
|
|
467
|
+
return { resources: [], resourceTemplates: [] };
|
|
468
|
+
}
|
|
469
|
+
const [resources, resourceTemplates] = await Promise.all([
|
|
470
|
+
client.listResources(undefined, this.#requestOptions(signal))
|
|
471
|
+
.then((result) => result.resources),
|
|
472
|
+
client.listResourceTemplates(undefined, this.#requestOptions(signal))
|
|
473
|
+
.then((result) => result.resourceTemplates),
|
|
474
|
+
]);
|
|
475
|
+
return { resources, resourceTemplates };
|
|
476
|
+
});
|
|
136
477
|
}
|
|
137
|
-
async
|
|
138
|
-
return (
|
|
478
|
+
async prompts(signal) {
|
|
479
|
+
return this.#request(async (client) => {
|
|
480
|
+
if (serverCapabilities(client)?.prompts === undefined)
|
|
481
|
+
return [];
|
|
482
|
+
return (await client.listPrompts(undefined, this.#requestOptions(signal))).prompts;
|
|
483
|
+
});
|
|
139
484
|
}
|
|
140
|
-
|
|
141
|
-
return {
|
|
485
|
+
async callTool(name, args, signal, onProgress, interact, toolDefinition) {
|
|
486
|
+
return this.#request(async (client, subscriptions, extensions) => {
|
|
487
|
+
const timeout = requestTimeoutMs(this.#environ);
|
|
488
|
+
if (serverSupportsTasks(serverCapabilities(client))) {
|
|
489
|
+
const tool = toolDefinition ?? (await client.listTools(undefined, this.#requestOptions(signal))).tools.find((candidate) => candidate.name === name);
|
|
490
|
+
if (tool === undefined) {
|
|
491
|
+
throw new Error(`MCP server '${this.#definition.name}' did not list tool '${name}'.`);
|
|
492
|
+
}
|
|
493
|
+
return callToolWithTasks({
|
|
494
|
+
server: this.#definition.name,
|
|
495
|
+
name,
|
|
496
|
+
args,
|
|
497
|
+
tool,
|
|
498
|
+
signal,
|
|
499
|
+
onProgress,
|
|
500
|
+
interact,
|
|
501
|
+
timeout,
|
|
502
|
+
channel: extensions ?? (() => { throw new Error("MCP tasks channel absent on a non-modern connection."); })(),
|
|
503
|
+
subscriptions,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return runInputRequiredRequest({
|
|
507
|
+
server: this.#definition.name,
|
|
508
|
+
operation: "tools/call",
|
|
509
|
+
originalParams: { name, arguments: args },
|
|
510
|
+
signal,
|
|
511
|
+
interact,
|
|
512
|
+
onProgress,
|
|
513
|
+
timeout,
|
|
514
|
+
requestLeg: (params, options) => client.callTool(params, {
|
|
515
|
+
...options,
|
|
516
|
+
...(toolDefinition === undefined ? {} : { toolDefinition }),
|
|
517
|
+
}),
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
async readResource(uri, signal, interact) {
|
|
522
|
+
return this.#request(async (client, subscriptions) => {
|
|
523
|
+
await subscriptions.selectResource(uri);
|
|
524
|
+
return runInputRequiredRequest({
|
|
525
|
+
server: this.#definition.name,
|
|
526
|
+
operation: "resources/read",
|
|
527
|
+
originalParams: { uri },
|
|
528
|
+
signal,
|
|
529
|
+
interact,
|
|
530
|
+
timeout: requestTimeoutMs(this.#environ),
|
|
531
|
+
requestLeg: (params, options, retry) => client.readResource(params, retry ? { ...options, cacheMode: "refresh" } : options),
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async getPrompt(name, args, signal, interact) {
|
|
536
|
+
return this.#request(async (client) => runInputRequiredRequest({
|
|
537
|
+
server: this.#definition.name,
|
|
538
|
+
operation: "prompts/get",
|
|
539
|
+
originalParams: { name, ...(args === undefined ? {} : { arguments: args }) },
|
|
142
540
|
signal,
|
|
541
|
+
interact,
|
|
143
542
|
timeout: requestTimeoutMs(this.#environ),
|
|
543
|
+
requestLeg: (params, options) => client.getPrompt(params, options),
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
async complete(params, signal) {
|
|
547
|
+
return this.#request(async (client) => client.complete(params, this.#requestOptions(signal)));
|
|
548
|
+
}
|
|
549
|
+
#requestOptions(signal, onProgress) {
|
|
550
|
+
const timeout = requestTimeoutMs(this.#environ);
|
|
551
|
+
return {
|
|
552
|
+
signal,
|
|
553
|
+
timeout,
|
|
554
|
+
maxTotalTimeout: timeout,
|
|
555
|
+
...(onProgress === undefined ? {} : { onprogress: onProgress }),
|
|
144
556
|
};
|
|
145
557
|
}
|
|
146
558
|
async close() {
|
|
559
|
+
if (this.#closed)
|
|
560
|
+
return;
|
|
561
|
+
this.#closed = true;
|
|
147
562
|
const client = this.#client;
|
|
148
563
|
this.#client = undefined;
|
|
149
|
-
|
|
150
|
-
|
|
564
|
+
const pending = this.#pendingAuthorization;
|
|
565
|
+
this.#pendingAuthorization = undefined;
|
|
566
|
+
const closures = [];
|
|
567
|
+
if (client !== undefined) {
|
|
568
|
+
closures.push(client.then(async ({ client: connected, extensions, subscriptions }) => {
|
|
569
|
+
const failures = [];
|
|
570
|
+
extensions?.close();
|
|
571
|
+
// A full connection close terminates its listen request. Retiring first
|
|
572
|
+
// avoids a redundant cancellation racing the SDK's removed listen ID.
|
|
573
|
+
const settled = await Promise.allSettled([
|
|
574
|
+
subscriptions.retire(),
|
|
575
|
+
connected.close(),
|
|
576
|
+
]);
|
|
577
|
+
failures.push(...settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
|
|
578
|
+
if (failures.length === 1)
|
|
579
|
+
throw failures[0];
|
|
580
|
+
if (failures.length > 1) {
|
|
581
|
+
throw new AggregateError(failures, `MCP server '${this.#definition.name}' connection shutdown failed.`);
|
|
582
|
+
}
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
if (pending?.standaloneTransport === true)
|
|
586
|
+
closures.push(pending.transport.close());
|
|
587
|
+
const settled = await Promise.allSettled(closures);
|
|
588
|
+
const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
589
|
+
if (failures.length === 1)
|
|
590
|
+
throw failures[0];
|
|
591
|
+
if (failures.length > 1) {
|
|
592
|
+
throw new AggregateError(failures, `MCP server '${this.#definition.name}' shutdown failed.`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
describeError(error) {
|
|
596
|
+
return message(error);
|
|
151
597
|
}
|
|
152
598
|
}
|
|
153
599
|
//# sourceMappingURL=client.js.map
|