@executor-js/plugin-mcp 0.1.0 → 1.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AddMcpSource-TLAL463B.js +762 -0
- package/dist/AddMcpSource-TLAL463B.js.map +1 -0
- package/dist/EditMcpSource-CWN6HIC4.js +259 -0
- package/dist/EditMcpSource-CWN6HIC4.js.map +1 -0
- package/dist/McpSourceSummary-257JNETP.js +85 -0
- package/dist/McpSourceSummary-257JNETP.js.map +1 -0
- package/dist/api/group.d.ts +183 -19
- package/dist/api/index.d.ts +501 -0
- package/dist/chunk-4ORPFRLI.js +238 -0
- package/dist/chunk-4ORPFRLI.js.map +1 -0
- package/dist/chunk-M6REVU6O.js +179 -0
- package/dist/chunk-M6REVU6O.js.map +1 -0
- package/dist/chunk-NQT7NAGE.js +2277 -0
- package/dist/chunk-NQT7NAGE.js.map +1 -0
- package/dist/chunk-SKSXXFOA.js +104 -0
- package/dist/chunk-SKSXXFOA.js.map +1 -0
- package/dist/chunk-ZIRGIRGP.js +115 -0
- package/dist/chunk-ZIRGIRGP.js.map +1 -0
- package/dist/client.js +51 -0
- package/dist/client.js.map +1 -0
- package/dist/core.js +26 -2
- package/dist/index.js +2 -1
- package/dist/react/McpRemoteSourceFields.d.ts +18 -0
- package/dist/react/McpSourceSummary.d.ts +5 -0
- package/dist/react/atoms.d.ts +286 -6
- package/dist/react/client.d.ts +187 -16
- package/dist/react/index.d.ts +1 -1
- package/dist/react/plugin-client.d.ts +9 -2
- package/dist/sdk/binding-store.d.ts +106 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/invoke.d.ts +2 -0
- package/dist/sdk/plugin.d.ts +178 -114
- package/dist/sdk/probe-shape-real-servers.live.test.d.ts +1 -0
- package/dist/sdk/probe-shape.d.ts +17 -3
- package/dist/sdk/stored-source.d.ts +12 -11
- package/dist/sdk/types.d.ts +122 -17
- package/dist/{stdio-connector-KNHLETKM.js → stdio-connector-AA5S6UUJ.js} +1 -1
- package/dist/{stdio-connector-KNHLETKM.js.map → stdio-connector-AA5S6UUJ.js.map} +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/{sdk/test-utils.d.ts → testing/server.d.ts} +0 -6
- package/dist/testing.js +51 -0
- package/dist/testing.js.map +1 -0
- package/package.json +17 -4
- package/dist/chunk-C2GNZGFJ.js +0 -1622
- package/dist/chunk-C2GNZGFJ.js.map +0 -1
|
@@ -0,0 +1,2277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_HEADER_AUTH_SLOT,
|
|
3
|
+
MCP_OAUTH_CLIENT_ID_SLOT,
|
|
4
|
+
MCP_OAUTH_CLIENT_SECRET_SLOT,
|
|
5
|
+
MCP_OAUTH_CONNECTION_SLOT,
|
|
6
|
+
McpConnectionError,
|
|
7
|
+
McpInvocationError,
|
|
8
|
+
McpSourceBindingRef,
|
|
9
|
+
McpStoredSourceData,
|
|
10
|
+
McpToolAnnotations,
|
|
11
|
+
McpToolBinding,
|
|
12
|
+
McpToolDiscoveryError,
|
|
13
|
+
mcpHeaderSlot,
|
|
14
|
+
mcpQueryParamSlot
|
|
15
|
+
} from "./chunk-M6REVU6O.js";
|
|
16
|
+
|
|
17
|
+
// src/sdk/binding-store.ts
|
|
18
|
+
import { Effect, Option, Schema } from "effect";
|
|
19
|
+
import {
|
|
20
|
+
ConfiguredCredentialBinding,
|
|
21
|
+
defineSchema
|
|
22
|
+
} from "@executor-js/sdk/core";
|
|
23
|
+
var mcpSchema = defineSchema({
|
|
24
|
+
mcp_source: {
|
|
25
|
+
fields: {
|
|
26
|
+
id: { type: "string", required: true },
|
|
27
|
+
scope_id: { type: "string", required: true, index: true },
|
|
28
|
+
name: { type: "string", required: true },
|
|
29
|
+
// Plugin-private structural data minus the ref-bearing fields
|
|
30
|
+
// (auth, headers, queryParams). For remote sources: transport,
|
|
31
|
+
// endpoint, remoteTransport. For stdio: transport, command,
|
|
32
|
+
// args, env, cwd.
|
|
33
|
+
config: { type: "json", required: true },
|
|
34
|
+
// Flattened McpConnectionAuth. The stored source only names slots;
|
|
35
|
+
// concrete per-user/per-workspace values live in core credential_binding.
|
|
36
|
+
auth_kind: {
|
|
37
|
+
type: ["none", "header", "oauth2"],
|
|
38
|
+
required: true,
|
|
39
|
+
defaultValue: "none"
|
|
40
|
+
},
|
|
41
|
+
// Header-auth fields.
|
|
42
|
+
auth_header_name: { type: "string", required: false },
|
|
43
|
+
auth_header_slot: { type: "string", required: false },
|
|
44
|
+
auth_header_prefix: { type: "string", required: false },
|
|
45
|
+
// OAuth2 auth fields.
|
|
46
|
+
auth_connection_slot: { type: "string", required: false },
|
|
47
|
+
auth_client_id_slot: {
|
|
48
|
+
type: "string",
|
|
49
|
+
required: false
|
|
50
|
+
},
|
|
51
|
+
auth_client_secret_slot: {
|
|
52
|
+
type: "string",
|
|
53
|
+
required: false
|
|
54
|
+
},
|
|
55
|
+
created_at: { type: "date", required: true }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
mcp_source_header: {
|
|
59
|
+
fields: {
|
|
60
|
+
id: { type: "string", required: true },
|
|
61
|
+
scope_id: { type: "string", required: true, index: true },
|
|
62
|
+
source_id: { type: "string", required: true, index: true },
|
|
63
|
+
name: { type: "string", required: true },
|
|
64
|
+
kind: { type: ["text", "binding"], required: true },
|
|
65
|
+
text_value: { type: "string", required: false },
|
|
66
|
+
slot_key: { type: "string", required: false },
|
|
67
|
+
prefix: { type: "string", required: false }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
mcp_source_query_param: {
|
|
71
|
+
fields: {
|
|
72
|
+
id: { type: "string", required: true },
|
|
73
|
+
scope_id: { type: "string", required: true, index: true },
|
|
74
|
+
source_id: { type: "string", required: true, index: true },
|
|
75
|
+
name: { type: "string", required: true },
|
|
76
|
+
kind: { type: ["text", "binding"], required: true },
|
|
77
|
+
text_value: { type: "string", required: false },
|
|
78
|
+
slot_key: { type: "string", required: false },
|
|
79
|
+
prefix: { type: "string", required: false }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
mcp_binding: {
|
|
83
|
+
fields: {
|
|
84
|
+
id: { type: "string", required: true },
|
|
85
|
+
scope_id: { type: "string", required: true, index: true },
|
|
86
|
+
source_id: { type: "string", required: true, index: true },
|
|
87
|
+
binding: { type: "json", required: true },
|
|
88
|
+
created_at: { type: "date", required: true }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
var decodeSourceData = Schema.decodeUnknownSync(McpStoredSourceData);
|
|
93
|
+
var encodeSourceData = Schema.encodeSync(McpStoredSourceData);
|
|
94
|
+
var decodeBinding = Schema.decodeUnknownSync(McpToolBinding);
|
|
95
|
+
var encodeBinding = Schema.encodeSync(McpToolBinding);
|
|
96
|
+
var decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
|
|
97
|
+
var coerceJson = (value) => {
|
|
98
|
+
if (typeof value !== "string") return value;
|
|
99
|
+
return Option.getOrElse(decodeJson(value), () => value);
|
|
100
|
+
};
|
|
101
|
+
var authToColumns = (auth) => {
|
|
102
|
+
if (auth.kind === "header") {
|
|
103
|
+
return {
|
|
104
|
+
auth_kind: "header",
|
|
105
|
+
auth_header_name: auth.headerName,
|
|
106
|
+
auth_header_slot: auth.secretSlot,
|
|
107
|
+
auth_header_prefix: auth.prefix
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (auth.kind === "oauth2") {
|
|
111
|
+
return {
|
|
112
|
+
auth_kind: "oauth2",
|
|
113
|
+
auth_connection_slot: auth.connectionSlot,
|
|
114
|
+
auth_client_id_slot: auth.clientIdSlot,
|
|
115
|
+
auth_client_secret_slot: auth.clientSecretSlot
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { auth_kind: "none" };
|
|
119
|
+
};
|
|
120
|
+
var columnsToAuth = (row) => {
|
|
121
|
+
const kind = row.auth_kind;
|
|
122
|
+
if (kind === "header" && typeof row.auth_header_slot === "string") {
|
|
123
|
+
const prefix = row.auth_header_prefix;
|
|
124
|
+
return {
|
|
125
|
+
kind: "header",
|
|
126
|
+
headerName: row.auth_header_name ?? "",
|
|
127
|
+
secretSlot: row.auth_header_slot,
|
|
128
|
+
...prefix ? { prefix } : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (kind === "oauth2" && typeof row.auth_connection_slot === "string") {
|
|
132
|
+
const cid = row.auth_client_id_slot;
|
|
133
|
+
const csec = row.auth_client_secret_slot;
|
|
134
|
+
return {
|
|
135
|
+
kind: "oauth2",
|
|
136
|
+
connectionSlot: row.auth_connection_slot,
|
|
137
|
+
...cid ? { clientIdSlot: cid } : {},
|
|
138
|
+
...csec ? { clientSecretSlot: csec } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { kind: "none" };
|
|
142
|
+
};
|
|
143
|
+
var valueMapToRows = (sourceId, scope, values) => {
|
|
144
|
+
if (!values) return [];
|
|
145
|
+
return Object.entries(values).map(([name, value]) => {
|
|
146
|
+
const id = JSON.stringify([sourceId, name]);
|
|
147
|
+
if (typeof value === "string") {
|
|
148
|
+
return {
|
|
149
|
+
id,
|
|
150
|
+
scope_id: scope,
|
|
151
|
+
source_id: sourceId,
|
|
152
|
+
name,
|
|
153
|
+
kind: "text",
|
|
154
|
+
text_value: value
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
scope_id: scope,
|
|
160
|
+
source_id: sourceId,
|
|
161
|
+
name,
|
|
162
|
+
kind: "binding",
|
|
163
|
+
slot_key: value.slot,
|
|
164
|
+
prefix: value.prefix
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
var rowsToValueMap = (rows) => {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
if (typeof row.name !== "string") continue;
|
|
172
|
+
const name = row.name;
|
|
173
|
+
if (row.kind === "binding" && typeof row.slot_key === "string") {
|
|
174
|
+
const prefix = row.prefix;
|
|
175
|
+
out[name] = prefix ? ConfiguredCredentialBinding.make({ kind: "binding", slot: row.slot_key, prefix }) : ConfiguredCredentialBinding.make({ kind: "binding", slot: row.slot_key });
|
|
176
|
+
} else if (row.kind === "text" && typeof row.text_value === "string") {
|
|
177
|
+
out[name] = row.text_value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
};
|
|
182
|
+
var makeMcpStore = ({ adapter: db }) => {
|
|
183
|
+
return {
|
|
184
|
+
listBindingsBySource: (namespace, scope) => Effect.gen(function* () {
|
|
185
|
+
const rows = yield* db.findMany({
|
|
186
|
+
model: "mcp_binding",
|
|
187
|
+
where: [
|
|
188
|
+
{ field: "source_id", value: namespace },
|
|
189
|
+
{ field: "scope_id", value: scope }
|
|
190
|
+
]
|
|
191
|
+
});
|
|
192
|
+
return rows.map((row) => ({
|
|
193
|
+
toolId: row.id,
|
|
194
|
+
binding: decodeBinding(coerceJson(row.binding))
|
|
195
|
+
}));
|
|
196
|
+
}),
|
|
197
|
+
getBinding: (toolId, scope) => Effect.gen(function* () {
|
|
198
|
+
const row = yield* db.findOne({
|
|
199
|
+
model: "mcp_binding",
|
|
200
|
+
where: [
|
|
201
|
+
{ field: "id", value: toolId },
|
|
202
|
+
{ field: "scope_id", value: scope }
|
|
203
|
+
]
|
|
204
|
+
});
|
|
205
|
+
if (!row) return null;
|
|
206
|
+
const binding = decodeBinding(coerceJson(row.binding));
|
|
207
|
+
return { binding, namespace: row.source_id };
|
|
208
|
+
}),
|
|
209
|
+
putBindings: (namespace, scope, entries) => Effect.gen(function* () {
|
|
210
|
+
if (entries.length === 0) return;
|
|
211
|
+
const now = /* @__PURE__ */ new Date();
|
|
212
|
+
yield* db.createMany({
|
|
213
|
+
model: "mcp_binding",
|
|
214
|
+
data: entries.map((e) => ({
|
|
215
|
+
id: e.toolId,
|
|
216
|
+
scope_id: scope,
|
|
217
|
+
source_id: namespace,
|
|
218
|
+
binding: encodeBinding(e.binding),
|
|
219
|
+
created_at: now
|
|
220
|
+
})),
|
|
221
|
+
forceAllowId: true
|
|
222
|
+
});
|
|
223
|
+
}),
|
|
224
|
+
removeBindingsByNamespace: (namespace, scope) => db.deleteMany({
|
|
225
|
+
model: "mcp_binding",
|
|
226
|
+
where: [
|
|
227
|
+
{ field: "source_id", value: namespace },
|
|
228
|
+
{ field: "scope_id", value: scope }
|
|
229
|
+
]
|
|
230
|
+
}).pipe(Effect.asVoid),
|
|
231
|
+
getSource: (namespace, scope) => Effect.gen(function* () {
|
|
232
|
+
const row = yield* db.findOne({
|
|
233
|
+
model: "mcp_source",
|
|
234
|
+
where: [
|
|
235
|
+
{ field: "id", value: namespace },
|
|
236
|
+
{ field: "scope_id", value: scope }
|
|
237
|
+
]
|
|
238
|
+
});
|
|
239
|
+
if (!row) return null;
|
|
240
|
+
return {
|
|
241
|
+
namespace: row.id,
|
|
242
|
+
scope: row.scope_id,
|
|
243
|
+
name: row.name,
|
|
244
|
+
config: yield* hydrateSourceData(row, namespace, scope)
|
|
245
|
+
};
|
|
246
|
+
}),
|
|
247
|
+
getSourceConfig: (namespace, scope) => Effect.gen(function* () {
|
|
248
|
+
const row = yield* db.findOne({
|
|
249
|
+
model: "mcp_source",
|
|
250
|
+
where: [
|
|
251
|
+
{ field: "id", value: namespace },
|
|
252
|
+
{ field: "scope_id", value: scope }
|
|
253
|
+
]
|
|
254
|
+
});
|
|
255
|
+
if (!row) return null;
|
|
256
|
+
return yield* hydrateSourceData(row, namespace, scope);
|
|
257
|
+
}),
|
|
258
|
+
putSource: (source) => Effect.gen(function* () {
|
|
259
|
+
const now = /* @__PURE__ */ new Date();
|
|
260
|
+
yield* db.delete({
|
|
261
|
+
model: "mcp_source",
|
|
262
|
+
where: [
|
|
263
|
+
{ field: "id", value: source.namespace },
|
|
264
|
+
{ field: "scope_id", value: source.scope }
|
|
265
|
+
]
|
|
266
|
+
});
|
|
267
|
+
yield* deleteSourceChildren(source.namespace, source.scope);
|
|
268
|
+
const auth = source.config.transport === "remote" ? source.config.auth : { kind: "none" };
|
|
269
|
+
const authCols = authToColumns(auth);
|
|
270
|
+
const headers = source.config.transport === "remote" ? source.config.headers : void 0;
|
|
271
|
+
const queryParams = source.config.transport === "remote" ? source.config.queryParams : void 0;
|
|
272
|
+
const encodedConfig = stripExtractedFields(
|
|
273
|
+
encodeSourceData(source.config)
|
|
274
|
+
);
|
|
275
|
+
yield* db.create({
|
|
276
|
+
model: "mcp_source",
|
|
277
|
+
data: {
|
|
278
|
+
id: source.namespace,
|
|
279
|
+
scope_id: source.scope,
|
|
280
|
+
name: source.name,
|
|
281
|
+
config: encodedConfig,
|
|
282
|
+
created_at: now,
|
|
283
|
+
...authCols
|
|
284
|
+
},
|
|
285
|
+
forceAllowId: true
|
|
286
|
+
});
|
|
287
|
+
const headerRows = valueMapToRows(source.namespace, source.scope, headers);
|
|
288
|
+
if (headerRows.length > 0) {
|
|
289
|
+
yield* db.createMany({
|
|
290
|
+
model: "mcp_source_header",
|
|
291
|
+
data: headerRows,
|
|
292
|
+
forceAllowId: true
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
const paramRows = valueMapToRows(source.namespace, source.scope, queryParams);
|
|
296
|
+
if (paramRows.length > 0) {
|
|
297
|
+
yield* db.createMany({
|
|
298
|
+
model: "mcp_source_query_param",
|
|
299
|
+
data: paramRows,
|
|
300
|
+
forceAllowId: true
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}),
|
|
304
|
+
removeSource: (namespace, scope) => Effect.gen(function* () {
|
|
305
|
+
yield* db.deleteMany({
|
|
306
|
+
model: "mcp_binding",
|
|
307
|
+
where: [
|
|
308
|
+
{ field: "source_id", value: namespace },
|
|
309
|
+
{ field: "scope_id", value: scope }
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
yield* deleteSourceChildren(namespace, scope);
|
|
313
|
+
yield* db.delete({
|
|
314
|
+
model: "mcp_source",
|
|
315
|
+
where: [
|
|
316
|
+
{ field: "id", value: namespace },
|
|
317
|
+
{ field: "scope_id", value: scope }
|
|
318
|
+
]
|
|
319
|
+
});
|
|
320
|
+
})
|
|
321
|
+
};
|
|
322
|
+
function deleteSourceChildren(namespace, scope) {
|
|
323
|
+
return Effect.gen(function* () {
|
|
324
|
+
for (const model of ["mcp_source_header", "mcp_source_query_param"]) {
|
|
325
|
+
yield* db.deleteMany({
|
|
326
|
+
model,
|
|
327
|
+
where: [
|
|
328
|
+
{ field: "source_id", value: namespace },
|
|
329
|
+
{ field: "scope_id", value: scope }
|
|
330
|
+
]
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function hydrateSourceData(row, namespace, scope) {
|
|
336
|
+
return Effect.gen(function* () {
|
|
337
|
+
const partial = coerceJson(row.config);
|
|
338
|
+
if (partial.transport !== "remote") {
|
|
339
|
+
return decodeSourceData(partial);
|
|
340
|
+
}
|
|
341
|
+
const headerRows = yield* db.findMany({
|
|
342
|
+
model: "mcp_source_header",
|
|
343
|
+
where: [
|
|
344
|
+
{ field: "source_id", value: namespace },
|
|
345
|
+
{ field: "scope_id", value: scope }
|
|
346
|
+
]
|
|
347
|
+
});
|
|
348
|
+
const paramRows = yield* db.findMany({
|
|
349
|
+
model: "mcp_source_query_param",
|
|
350
|
+
where: [
|
|
351
|
+
{ field: "source_id", value: namespace },
|
|
352
|
+
{ field: "scope_id", value: scope }
|
|
353
|
+
]
|
|
354
|
+
});
|
|
355
|
+
const headers = rowsToValueMap(headerRows);
|
|
356
|
+
const queryParams = rowsToValueMap(paramRows);
|
|
357
|
+
const reassembled = {
|
|
358
|
+
...partial,
|
|
359
|
+
...Object.keys(headers).length > 0 ? { headers } : {},
|
|
360
|
+
...Object.keys(queryParams).length > 0 ? { queryParams } : {},
|
|
361
|
+
auth: columnsToAuth(row)
|
|
362
|
+
};
|
|
363
|
+
return decodeSourceData(reassembled);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var stripExtractedFields = (encoded) => {
|
|
368
|
+
if (encoded.transport !== "remote") return encoded;
|
|
369
|
+
const { auth, headers, queryParams, ...rest } = encoded;
|
|
370
|
+
void auth;
|
|
371
|
+
void headers;
|
|
372
|
+
void queryParams;
|
|
373
|
+
return rest;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// src/sdk/plugin.ts
|
|
377
|
+
import {
|
|
378
|
+
Duration as Duration2,
|
|
379
|
+
Effect as Effect6,
|
|
380
|
+
Exit as Exit2,
|
|
381
|
+
Match,
|
|
382
|
+
Option as Option5,
|
|
383
|
+
Predicate as Predicate2,
|
|
384
|
+
Result,
|
|
385
|
+
Scope,
|
|
386
|
+
ScopedCache as ScopedCache2
|
|
387
|
+
} from "effect";
|
|
388
|
+
import {
|
|
389
|
+
ConfiguredCredentialBinding as ConfiguredCredentialBinding2,
|
|
390
|
+
ConnectionId,
|
|
391
|
+
ScopeId,
|
|
392
|
+
SecretId,
|
|
393
|
+
SourceDetectionResult,
|
|
394
|
+
definePlugin,
|
|
395
|
+
resolveSecretBackedMap as resolveSharedSecretBackedMap,
|
|
396
|
+
StorageError
|
|
397
|
+
} from "@executor-js/sdk/core";
|
|
398
|
+
|
|
399
|
+
// src/sdk/connection.ts
|
|
400
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
401
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
402
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
403
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
404
|
+
import { Effect as Effect2 } from "effect";
|
|
405
|
+
var buildEndpointUrl = (endpoint, queryParams) => {
|
|
406
|
+
const url = new URL(endpoint);
|
|
407
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
408
|
+
url.searchParams.set(key, value);
|
|
409
|
+
}
|
|
410
|
+
return url;
|
|
411
|
+
};
|
|
412
|
+
var createClient = () => new Client(
|
|
413
|
+
{ name: "executor-mcp", version: "0.1.0" },
|
|
414
|
+
{
|
|
415
|
+
capabilities: { elicitation: { form: {}, url: {} } },
|
|
416
|
+
jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
var connectionFromClient = (client) => ({
|
|
420
|
+
client,
|
|
421
|
+
close: () => client.close()
|
|
422
|
+
});
|
|
423
|
+
var connectClient = (input) => Effect2.gen(function* () {
|
|
424
|
+
const client = createClient();
|
|
425
|
+
const transportInstance = input.createTransport();
|
|
426
|
+
yield* Effect2.tryPromise({
|
|
427
|
+
try: () => client.connect(transportInstance),
|
|
428
|
+
catch: () => new McpConnectionError({
|
|
429
|
+
transport: input.transport,
|
|
430
|
+
message: `Failed connecting via ${input.transport}`
|
|
431
|
+
})
|
|
432
|
+
}).pipe(
|
|
433
|
+
Effect2.withSpan("plugin.mcp.connection.handshake", {
|
|
434
|
+
attributes: { "plugin.mcp.transport": input.transport }
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
return connectionFromClient(client);
|
|
438
|
+
});
|
|
439
|
+
var createMcpConnector = (input) => {
|
|
440
|
+
if (input.transport === "stdio") {
|
|
441
|
+
const command = input.command.trim();
|
|
442
|
+
if (!command) {
|
|
443
|
+
return Effect2.fail(
|
|
444
|
+
new McpConnectionError({
|
|
445
|
+
transport: "stdio",
|
|
446
|
+
message: "MCP stdio transport requires a command"
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return Effect2.gen(function* () {
|
|
451
|
+
const { createStdioTransport } = yield* Effect2.tryPromise({
|
|
452
|
+
try: () => import("./stdio-connector-AA5S6UUJ.js"),
|
|
453
|
+
catch: () => new McpConnectionError({
|
|
454
|
+
transport: "stdio",
|
|
455
|
+
message: "Failed to load stdio transport module"
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
return yield* connectClient({
|
|
459
|
+
transport: "stdio",
|
|
460
|
+
createTransport: () => createStdioTransport({
|
|
461
|
+
command,
|
|
462
|
+
args: input.args,
|
|
463
|
+
env: input.env,
|
|
464
|
+
cwd: input.cwd?.trim().length ? input.cwd.trim() : void 0
|
|
465
|
+
})
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const headers = input.headers ?? {};
|
|
470
|
+
const remoteTransport = input.remoteTransport ?? "auto";
|
|
471
|
+
const requestInit = Object.keys(headers).length > 0 ? { headers } : void 0;
|
|
472
|
+
const endpoint = buildEndpointUrl(input.endpoint, input.queryParams ?? {});
|
|
473
|
+
const connectStreamableHttp = connectClient({
|
|
474
|
+
transport: "streamable-http",
|
|
475
|
+
createTransport: () => new StreamableHTTPClientTransport(endpoint, {
|
|
476
|
+
requestInit,
|
|
477
|
+
authProvider: input.authProvider
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
const connectSse = connectClient({
|
|
481
|
+
transport: "sse",
|
|
482
|
+
createTransport: () => new SSEClientTransport(endpoint, {
|
|
483
|
+
requestInit,
|
|
484
|
+
authProvider: input.authProvider
|
|
485
|
+
})
|
|
486
|
+
});
|
|
487
|
+
if (remoteTransport === "streamable-http") return connectStreamableHttp;
|
|
488
|
+
if (remoteTransport === "sse") return connectSse;
|
|
489
|
+
return connectStreamableHttp.pipe(Effect2.catch(() => connectSse));
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/sdk/discover.ts
|
|
493
|
+
import { Effect as Effect3 } from "effect";
|
|
494
|
+
|
|
495
|
+
// src/sdk/manifest.ts
|
|
496
|
+
import { Option as Option2, Schema as Schema2 } from "effect";
|
|
497
|
+
var ListedTool = Schema2.Struct({
|
|
498
|
+
name: Schema2.String,
|
|
499
|
+
description: Schema2.optional(Schema2.NullOr(Schema2.String)),
|
|
500
|
+
inputSchema: Schema2.optional(Schema2.Unknown),
|
|
501
|
+
parameters: Schema2.optional(Schema2.Unknown),
|
|
502
|
+
outputSchema: Schema2.optional(Schema2.Unknown),
|
|
503
|
+
annotations: Schema2.optional(McpToolAnnotations)
|
|
504
|
+
});
|
|
505
|
+
var ListToolsResult = Schema2.Struct({
|
|
506
|
+
tools: Schema2.Array(ListedTool)
|
|
507
|
+
});
|
|
508
|
+
var ServerInfo = Schema2.Struct({
|
|
509
|
+
name: Schema2.optional(Schema2.String),
|
|
510
|
+
version: Schema2.optional(Schema2.String)
|
|
511
|
+
});
|
|
512
|
+
var decodeListToolsResult = Schema2.decodeUnknownOption(ListToolsResult);
|
|
513
|
+
var decodeServerInfo = Schema2.decodeUnknownOption(ServerInfo);
|
|
514
|
+
var isListToolsResult = (value) => Option2.isSome(decodeListToolsResult(value));
|
|
515
|
+
var sanitize = (value) => {
|
|
516
|
+
const s = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
517
|
+
return s || "tool";
|
|
518
|
+
};
|
|
519
|
+
var uniqueId = (value, seen) => {
|
|
520
|
+
const base = sanitize(value);
|
|
521
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
522
|
+
seen.set(base, n);
|
|
523
|
+
return n === 1 ? base : `${base}_${n}`;
|
|
524
|
+
};
|
|
525
|
+
var extractManifestFromListToolsResult = (listToolsResult, metadata) => {
|
|
526
|
+
const seen = /* @__PURE__ */ new Map();
|
|
527
|
+
const listed = decodeListToolsResult(listToolsResult).pipe(
|
|
528
|
+
Option2.map((result) => result.tools),
|
|
529
|
+
Option2.getOrElse(() => [])
|
|
530
|
+
);
|
|
531
|
+
const server = decodeServerInfo(metadata?.serverInfo).pipe(
|
|
532
|
+
Option2.map(
|
|
533
|
+
(info) => ({
|
|
534
|
+
name: info.name ?? null,
|
|
535
|
+
version: info.version ?? null
|
|
536
|
+
})
|
|
537
|
+
),
|
|
538
|
+
Option2.getOrNull
|
|
539
|
+
);
|
|
540
|
+
const tools = listed.flatMap((tool) => {
|
|
541
|
+
const toolName = tool.name.trim();
|
|
542
|
+
if (!toolName) return [];
|
|
543
|
+
return [
|
|
544
|
+
{
|
|
545
|
+
toolId: uniqueId(toolName, seen),
|
|
546
|
+
toolName,
|
|
547
|
+
description: tool.description ?? null,
|
|
548
|
+
inputSchema: tool.inputSchema ?? tool.parameters,
|
|
549
|
+
outputSchema: tool.outputSchema,
|
|
550
|
+
annotations: tool.annotations
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
});
|
|
554
|
+
return { server, tools };
|
|
555
|
+
};
|
|
556
|
+
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
557
|
+
var hostnameOf = (url) => {
|
|
558
|
+
if (!URL.canParse(url)) return null;
|
|
559
|
+
return new URL(url).hostname;
|
|
560
|
+
};
|
|
561
|
+
var basenameOf = (path) => path.trim().split(/[\\/]/).pop() ?? path.trim();
|
|
562
|
+
var deriveMcpNamespace = (input) => {
|
|
563
|
+
if (input.name?.trim()) return slugify(input.name) || "mcp";
|
|
564
|
+
const fromEndpoint = input.endpoint?.trim() ? hostnameOf(input.endpoint) : null;
|
|
565
|
+
if (fromEndpoint) return slugify(fromEndpoint) || "mcp";
|
|
566
|
+
if (input.command?.trim()) return slugify(basenameOf(input.command)) || "mcp";
|
|
567
|
+
return "mcp";
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/sdk/discover.ts
|
|
571
|
+
var discoverTools = (connector) => Effect3.gen(function* () {
|
|
572
|
+
const connection = yield* connector.pipe(
|
|
573
|
+
Effect3.mapError(
|
|
574
|
+
({ message }) => new McpToolDiscoveryError({
|
|
575
|
+
stage: "connect",
|
|
576
|
+
message: `Failed connecting to MCP server: ${message}`
|
|
577
|
+
})
|
|
578
|
+
)
|
|
579
|
+
);
|
|
580
|
+
const listResult = yield* Effect3.tryPromise({
|
|
581
|
+
try: () => connection.client.listTools(),
|
|
582
|
+
catch: () => new McpToolDiscoveryError({
|
|
583
|
+
stage: "list_tools",
|
|
584
|
+
message: "Failed listing MCP tools"
|
|
585
|
+
})
|
|
586
|
+
});
|
|
587
|
+
if (!isListToolsResult(listResult)) {
|
|
588
|
+
yield* closeConnection(connection);
|
|
589
|
+
return yield* new McpToolDiscoveryError({
|
|
590
|
+
stage: "list_tools",
|
|
591
|
+
message: "MCP listTools response did not match the expected schema"
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
const manifest = extractManifestFromListToolsResult(listResult, {
|
|
595
|
+
serverInfo: connection.client.getServerVersion?.()
|
|
596
|
+
});
|
|
597
|
+
yield* closeConnection(connection);
|
|
598
|
+
return manifest;
|
|
599
|
+
});
|
|
600
|
+
var closeConnection = (connection) => Effect3.ignore(
|
|
601
|
+
Effect3.tryPromise({
|
|
602
|
+
try: () => connection.close(),
|
|
603
|
+
catch: () => new McpToolDiscoveryError({
|
|
604
|
+
stage: "list_tools",
|
|
605
|
+
message: "Failed closing MCP connection"
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// src/sdk/invoke.ts
|
|
611
|
+
import { Cause, Effect as Effect4, Exit, Option as Option3, Predicate, Schema as Schema3, ScopedCache } from "effect";
|
|
612
|
+
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
613
|
+
import {
|
|
614
|
+
FormElicitation,
|
|
615
|
+
UrlElicitation
|
|
616
|
+
} from "@executor-js/sdk/core";
|
|
617
|
+
var ArgsRecord = Schema3.Record(Schema3.String, Schema3.Unknown);
|
|
618
|
+
var decodeArgsRecord = Schema3.decodeUnknownOption(ArgsRecord);
|
|
619
|
+
var argsRecord = (value) => Option3.getOrElse(decodeArgsRecord(value), () => ({}));
|
|
620
|
+
var stableJson = (value) => {
|
|
621
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
622
|
+
if (value && typeof value === "object") {
|
|
623
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(",")}}`;
|
|
624
|
+
}
|
|
625
|
+
return JSON.stringify(value);
|
|
626
|
+
};
|
|
627
|
+
var fingerprint = (value) => {
|
|
628
|
+
const input = stableJson(value);
|
|
629
|
+
let hash = 2166136261;
|
|
630
|
+
for (let i = 0; i < input.length; i++) {
|
|
631
|
+
hash ^= input.charCodeAt(i);
|
|
632
|
+
hash = Math.imul(hash, 16777619);
|
|
633
|
+
}
|
|
634
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
635
|
+
};
|
|
636
|
+
var connectionCacheKey = (input) => {
|
|
637
|
+
const sd = input.sourceData;
|
|
638
|
+
return sd.transport === "stdio" ? `stdio:${fingerprint({
|
|
639
|
+
sourceId: input.sourceId,
|
|
640
|
+
sourceScope: input.sourceScope,
|
|
641
|
+
command: sd.command,
|
|
642
|
+
args: sd.args ?? [],
|
|
643
|
+
env: sd.env ?? {},
|
|
644
|
+
cwd: sd.cwd ?? null
|
|
645
|
+
})}` : `remote:${fingerprint({
|
|
646
|
+
sourceId: input.sourceId,
|
|
647
|
+
sourceScope: input.sourceScope,
|
|
648
|
+
invokerScope: input.invokerScope,
|
|
649
|
+
endpoint: sd.endpoint,
|
|
650
|
+
remoteTransport: sd.remoteTransport ?? "auto",
|
|
651
|
+
headers: sd.headers ?? {},
|
|
652
|
+
queryParams: sd.queryParams ?? {},
|
|
653
|
+
auth: sd.auth
|
|
654
|
+
})}`;
|
|
655
|
+
};
|
|
656
|
+
var McpElicitParams = Schema3.Union([
|
|
657
|
+
Schema3.Struct({
|
|
658
|
+
mode: Schema3.Literal("url"),
|
|
659
|
+
message: Schema3.String,
|
|
660
|
+
url: Schema3.String,
|
|
661
|
+
elicitationId: Schema3.optional(Schema3.String),
|
|
662
|
+
id: Schema3.optional(Schema3.String)
|
|
663
|
+
}),
|
|
664
|
+
Schema3.Struct({
|
|
665
|
+
mode: Schema3.optional(Schema3.Literal("form")),
|
|
666
|
+
message: Schema3.String,
|
|
667
|
+
requestedSchema: Schema3.Record(Schema3.String, Schema3.Unknown)
|
|
668
|
+
})
|
|
669
|
+
]);
|
|
670
|
+
var decodeElicitParams = Schema3.decodeUnknownSync(McpElicitParams);
|
|
671
|
+
var toElicitationRequest = (params) => params.mode === "url" ? UrlElicitation.make({
|
|
672
|
+
message: params.message,
|
|
673
|
+
url: params.url,
|
|
674
|
+
elicitationId: params.elicitationId ?? params.id ?? ""
|
|
675
|
+
}) : FormElicitation.make({
|
|
676
|
+
message: params.message,
|
|
677
|
+
requestedSchema: params.requestedSchema
|
|
678
|
+
});
|
|
679
|
+
var installElicitationHandler = (client, elicit) => {
|
|
680
|
+
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
|
681
|
+
const params = decodeElicitParams(request.params);
|
|
682
|
+
const req = toElicitationRequest(params);
|
|
683
|
+
const exit = await Effect4.runPromiseExit(elicit(req));
|
|
684
|
+
if (Exit.isSuccess(exit)) {
|
|
685
|
+
const response = exit.value;
|
|
686
|
+
return {
|
|
687
|
+
action: response.action,
|
|
688
|
+
...response.action === "accept" && response.content ? { content: response.content } : {}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const failure = exit.cause.reasons.find(Cause.isFailReason);
|
|
692
|
+
if (failure) {
|
|
693
|
+
const err = failure.error;
|
|
694
|
+
if (Predicate.isTagged(err, "ElicitationDeclinedError")) {
|
|
695
|
+
const action = Predicate.hasProperty(err, "action") && err.action === "cancel" ? "cancel" : "decline";
|
|
696
|
+
return { action };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
throw Cause.squash(exit.cause);
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
var useConnection = (connection, toolName, args, elicit) => Effect4.gen(function* () {
|
|
703
|
+
installElicitationHandler(connection.client, elicit);
|
|
704
|
+
return yield* Effect4.tryPromise({
|
|
705
|
+
try: () => connection.client.callTool({ name: toolName, arguments: args }),
|
|
706
|
+
catch: () => new McpInvocationError({
|
|
707
|
+
toolName,
|
|
708
|
+
message: `MCP tool call failed for ${toolName}`
|
|
709
|
+
})
|
|
710
|
+
}).pipe(
|
|
711
|
+
Effect4.withSpan("plugin.mcp.client.call_tool", {
|
|
712
|
+
attributes: { "mcp.tool.name": toolName }
|
|
713
|
+
})
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
var invokeMcpTool = (input) => {
|
|
717
|
+
const transport = input.sourceData.transport === "stdio" ? "stdio" : input.sourceData.remoteTransport ?? "auto";
|
|
718
|
+
return Effect4.gen(function* () {
|
|
719
|
+
const cacheKey = connectionCacheKey(input);
|
|
720
|
+
const args = argsRecord(input.args);
|
|
721
|
+
const connector = input.resolveConnector();
|
|
722
|
+
input.pendingConnectors.set(cacheKey, connector);
|
|
723
|
+
const cacheHit = yield* ScopedCache.has(input.connectionCache, cacheKey);
|
|
724
|
+
const firstConnection = yield* ScopedCache.get(input.connectionCache, cacheKey).pipe(
|
|
725
|
+
Effect4.withSpan("plugin.mcp.connection.acquire", {
|
|
726
|
+
attributes: {
|
|
727
|
+
"plugin.mcp.transport": transport,
|
|
728
|
+
"plugin.mcp.cache_key": cacheKey,
|
|
729
|
+
"plugin.mcp.attempt": 1,
|
|
730
|
+
"plugin.mcp.cache_hit": cacheHit
|
|
731
|
+
}
|
|
732
|
+
})
|
|
733
|
+
);
|
|
734
|
+
return yield* useConnection(firstConnection, input.toolName, args, input.elicit).pipe(
|
|
735
|
+
// On failure, invalidate the cache and retry once with a fresh
|
|
736
|
+
// connection. Matches the old invoker's retry-once semantics.
|
|
737
|
+
Effect4.catch(
|
|
738
|
+
() => Effect4.gen(function* () {
|
|
739
|
+
yield* ScopedCache.invalidate(input.connectionCache, cacheKey);
|
|
740
|
+
input.pendingConnectors.set(cacheKey, connector);
|
|
741
|
+
const fresh = yield* ScopedCache.get(input.connectionCache, cacheKey);
|
|
742
|
+
return yield* useConnection(fresh, input.toolName, args, input.elicit);
|
|
743
|
+
}).pipe(
|
|
744
|
+
Effect4.withSpan("plugin.mcp.invoke.retry", {
|
|
745
|
+
attributes: {
|
|
746
|
+
"plugin.mcp.transport": transport,
|
|
747
|
+
"plugin.mcp.cache_key": cacheKey,
|
|
748
|
+
"mcp.tool.name": input.toolName
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
}).pipe(
|
|
755
|
+
Effect4.scoped,
|
|
756
|
+
Effect4.withSpan("plugin.mcp.invoke", {
|
|
757
|
+
attributes: {
|
|
758
|
+
"mcp.tool.name": input.toolName,
|
|
759
|
+
"plugin.mcp.tool_id": input.toolId,
|
|
760
|
+
"plugin.mcp.transport": transport
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
);
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/sdk/probe-shape.ts
|
|
767
|
+
import { Data, Duration, Effect as Effect5, Option as Option4, Schema as Schema4 } from "effect";
|
|
768
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
769
|
+
var INITIALIZE_BODY = JSON.stringify({
|
|
770
|
+
jsonrpc: "2.0",
|
|
771
|
+
id: 1,
|
|
772
|
+
method: "initialize",
|
|
773
|
+
params: {
|
|
774
|
+
protocolVersion: "2025-06-18",
|
|
775
|
+
capabilities: {},
|
|
776
|
+
clientInfo: { name: "executor-probe", version: "0" }
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
var readHeader = (headers, name) => {
|
|
780
|
+
const direct = headers[name];
|
|
781
|
+
if (direct !== void 0) return direct;
|
|
782
|
+
const lower = name.toLowerCase();
|
|
783
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
784
|
+
if (k.toLowerCase() === lower) return v;
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
};
|
|
788
|
+
var ProbeTransportError = class extends Data.TaggedError("ProbeTransportError") {
|
|
789
|
+
};
|
|
790
|
+
var decodeJsonString = Schema4.decodeUnknownOption(Schema4.fromJsonString(Schema4.Unknown));
|
|
791
|
+
var asObject = (body) => {
|
|
792
|
+
if (!body) return null;
|
|
793
|
+
const parsed = decodeJsonString(body);
|
|
794
|
+
if (Option4.isNone(parsed)) return null;
|
|
795
|
+
const value = parsed.value;
|
|
796
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
797
|
+
return value;
|
|
798
|
+
};
|
|
799
|
+
var isJsonRpcEnvelope = (body) => {
|
|
800
|
+
const obj = asObject(body);
|
|
801
|
+
if (!obj) return false;
|
|
802
|
+
if (obj.jsonrpc !== "2.0") return false;
|
|
803
|
+
return "result" in obj || "error" in obj || "method" in obj;
|
|
804
|
+
};
|
|
805
|
+
var isOAuthErrorBody = (body) => {
|
|
806
|
+
const obj = asObject(body);
|
|
807
|
+
if (!obj) return false;
|
|
808
|
+
if (Array.isArray(obj.errors)) return false;
|
|
809
|
+
return typeof obj.error === "string";
|
|
810
|
+
};
|
|
811
|
+
var ErrorMessageShape = Schema4.Struct({ message: Schema4.String });
|
|
812
|
+
var decodeErrorMessageShape = Schema4.decodeUnknownOption(ErrorMessageShape);
|
|
813
|
+
var reasonFromBoundaryCause = (cause) => {
|
|
814
|
+
const messageShape = decodeErrorMessageShape(cause);
|
|
815
|
+
if (Option4.isSome(messageShape)) return messageShape.value.message;
|
|
816
|
+
if (typeof cause === "string") return cause;
|
|
817
|
+
if (typeof cause === "number" || typeof cause === "boolean" || typeof cause === "bigint") {
|
|
818
|
+
return `${cause}`;
|
|
819
|
+
}
|
|
820
|
+
if (typeof cause === "symbol") return cause.description ?? "symbol";
|
|
821
|
+
if (cause === null) return "null";
|
|
822
|
+
if (typeof cause === "undefined") return "undefined";
|
|
823
|
+
return "fetch failed";
|
|
824
|
+
};
|
|
825
|
+
var probeMcpEndpointShape = (endpoint, options = {}) => Effect5.gen(function* () {
|
|
826
|
+
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
827
|
+
const outcome = yield* Effect5.gen(function* () {
|
|
828
|
+
const client = yield* HttpClient.HttpClient;
|
|
829
|
+
const readBody = (response) => response.text.pipe(
|
|
830
|
+
Effect5.timeout(Duration.millis(timeoutMs)),
|
|
831
|
+
Effect5.catch(() => Effect5.succeed(""))
|
|
832
|
+
);
|
|
833
|
+
const classify = (response, method) => Effect5.gen(function* () {
|
|
834
|
+
const contentType = readHeader(response.headers, "content-type") ?? "";
|
|
835
|
+
const isSse = /^\s*text\/event-stream\b/i.test(contentType);
|
|
836
|
+
if (response.status === 401) {
|
|
837
|
+
const wwwAuth = readHeader(response.headers, "www-authenticate");
|
|
838
|
+
if (!wwwAuth || !/^\s*bearer\b/i.test(wwwAuth)) {
|
|
839
|
+
return {
|
|
840
|
+
kind: "not-mcp",
|
|
841
|
+
category: "auth-required",
|
|
842
|
+
reason: "401 without Bearer WWW-Authenticate \u2014 not an MCP auth challenge"
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (/(?:^|[\s,])resource_metadata\s*=/i.test(wwwAuth)) {
|
|
846
|
+
return { kind: "mcp", requiresAuth: true };
|
|
847
|
+
}
|
|
848
|
+
if (/(?:^|[\s,])error\s*=/i.test(wwwAuth)) {
|
|
849
|
+
return { kind: "mcp", requiresAuth: true };
|
|
850
|
+
}
|
|
851
|
+
if (isSse) return { kind: "mcp", requiresAuth: true };
|
|
852
|
+
const body = yield* readBody(response);
|
|
853
|
+
if (!isJsonRpcEnvelope(body) && !isOAuthErrorBody(body)) {
|
|
854
|
+
return {
|
|
855
|
+
kind: "not-mcp",
|
|
856
|
+
category: "auth-required",
|
|
857
|
+
reason: "401 + Bearer without resource_metadata, JSON-RPC body, or OAuth error body"
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return { kind: "mcp", requiresAuth: true };
|
|
861
|
+
}
|
|
862
|
+
if (response.status >= 200 && response.status < 300) {
|
|
863
|
+
if (method === "GET") {
|
|
864
|
+
if (!isSse) {
|
|
865
|
+
return {
|
|
866
|
+
kind: "not-mcp",
|
|
867
|
+
category: "wrong-shape",
|
|
868
|
+
reason: "GET response is not an SSE stream"
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return { kind: "mcp", requiresAuth: false };
|
|
872
|
+
}
|
|
873
|
+
if (isSse) return { kind: "mcp", requiresAuth: false };
|
|
874
|
+
const body = yield* readBody(response);
|
|
875
|
+
if (!isJsonRpcEnvelope(body)) {
|
|
876
|
+
return {
|
|
877
|
+
kind: "not-mcp",
|
|
878
|
+
category: "wrong-shape",
|
|
879
|
+
reason: "2xx POST body is not a JSON-RPC envelope"
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
return { kind: "mcp", requiresAuth: false };
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
});
|
|
886
|
+
const url = new URL(endpoint);
|
|
887
|
+
for (const [key, value] of Object.entries(options.queryParams ?? {})) {
|
|
888
|
+
url.searchParams.set(key, value);
|
|
889
|
+
}
|
|
890
|
+
let postRequest = HttpClientRequest.post(url.toString()).pipe(
|
|
891
|
+
HttpClientRequest.setHeader("content-type", "application/json"),
|
|
892
|
+
HttpClientRequest.setHeader("accept", "application/json, text/event-stream"),
|
|
893
|
+
HttpClientRequest.bodyText(INITIALIZE_BODY, "application/json")
|
|
894
|
+
);
|
|
895
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
896
|
+
postRequest = HttpClientRequest.setHeader(postRequest, name, value);
|
|
897
|
+
}
|
|
898
|
+
const postResponse = yield* client.execute(postRequest).pipe(Effect5.timeout(Duration.millis(timeoutMs)));
|
|
899
|
+
const postResult = yield* classify(postResponse, "POST");
|
|
900
|
+
if (postResult) return postResult;
|
|
901
|
+
if ([404, 405, 406, 415].includes(postResponse.status)) {
|
|
902
|
+
let getRequest = HttpClientRequest.get(url.toString()).pipe(
|
|
903
|
+
HttpClientRequest.setHeader("accept", "text/event-stream")
|
|
904
|
+
);
|
|
905
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
906
|
+
getRequest = HttpClientRequest.setHeader(getRequest, name, value);
|
|
907
|
+
}
|
|
908
|
+
const getResponse = yield* client.execute(getRequest).pipe(Effect5.timeout(Duration.millis(timeoutMs)));
|
|
909
|
+
const getResult = yield* classify(getResponse, "GET");
|
|
910
|
+
if (getResult) return getResult;
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
kind: "not-mcp",
|
|
914
|
+
category: "wrong-shape",
|
|
915
|
+
reason: `unexpected status ${postResponse.status} for initialize`
|
|
916
|
+
};
|
|
917
|
+
}).pipe(
|
|
918
|
+
Effect5.provide(options.httpClientLayer ?? FetchHttpClient.layer),
|
|
919
|
+
Effect5.mapError(
|
|
920
|
+
(cause) => new ProbeTransportError({
|
|
921
|
+
reason: reasonFromBoundaryCause(cause),
|
|
922
|
+
cause
|
|
923
|
+
})
|
|
924
|
+
),
|
|
925
|
+
Effect5.catch(
|
|
926
|
+
(cause) => Effect5.succeed({
|
|
927
|
+
kind: "unreachable",
|
|
928
|
+
reason: cause.reason
|
|
929
|
+
})
|
|
930
|
+
)
|
|
931
|
+
);
|
|
932
|
+
return outcome;
|
|
933
|
+
}).pipe(Effect5.withSpan("mcp.plugin.probe_shape"));
|
|
934
|
+
|
|
935
|
+
// src/sdk/plugin.ts
|
|
936
|
+
import {
|
|
937
|
+
SECRET_REF_PREFIX
|
|
938
|
+
} from "@executor-js/config";
|
|
939
|
+
var toStoredSourceData = (config, remoteCredentials) => {
|
|
940
|
+
if (config.transport === "stdio") {
|
|
941
|
+
return {
|
|
942
|
+
transport: "stdio",
|
|
943
|
+
command: config.command,
|
|
944
|
+
args: config.args,
|
|
945
|
+
env: config.env,
|
|
946
|
+
cwd: config.cwd
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
transport: "remote",
|
|
951
|
+
endpoint: config.endpoint,
|
|
952
|
+
remoteTransport: config.remoteTransport ?? "auto",
|
|
953
|
+
queryParams: remoteCredentials?.queryParams,
|
|
954
|
+
headers: remoteCredentials?.headers,
|
|
955
|
+
auth: remoteCredentials?.auth ?? { kind: "none" }
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
var normalizeNamespace = (config) => config.namespace ?? deriveMcpNamespace({
|
|
959
|
+
name: config.name,
|
|
960
|
+
endpoint: config.transport === "remote" ? config.endpoint : void 0,
|
|
961
|
+
command: config.transport === "stdio" ? config.command : void 0
|
|
962
|
+
});
|
|
963
|
+
var toBinding = (entry) => McpToolBinding.make({
|
|
964
|
+
toolId: entry.toolId,
|
|
965
|
+
toolName: entry.toolName,
|
|
966
|
+
description: entry.description,
|
|
967
|
+
inputSchema: entry.inputSchema,
|
|
968
|
+
outputSchema: entry.outputSchema,
|
|
969
|
+
annotations: entry.annotations
|
|
970
|
+
});
|
|
971
|
+
var MCP_PLUGIN_ID = "mcp";
|
|
972
|
+
var urlMatchesToken = (url, token) => {
|
|
973
|
+
const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i");
|
|
974
|
+
return re.test(url.hostname) || re.test(url.pathname);
|
|
975
|
+
};
|
|
976
|
+
var userFacingProbeMessage = (shape) => {
|
|
977
|
+
if (shape.kind === "unreachable") {
|
|
978
|
+
return "Couldn't reach this URL. Check the address, your network, and that the server is running.";
|
|
979
|
+
}
|
|
980
|
+
return Match.value(shape.category).pipe(
|
|
981
|
+
Match.when(
|
|
982
|
+
"auth-required",
|
|
983
|
+
() => "This server requires authentication. Add credentials (Authorization header, query parameter, or API key) below and retry."
|
|
984
|
+
),
|
|
985
|
+
Match.when(
|
|
986
|
+
"wrong-shape",
|
|
987
|
+
() => "This URL doesn't appear to host an MCP server. Double-check the address, including the path."
|
|
988
|
+
),
|
|
989
|
+
Match.exhaustive
|
|
990
|
+
);
|
|
991
|
+
};
|
|
992
|
+
var scopeRanks = (ctx) => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index]));
|
|
993
|
+
var scopeRank = (ranks, scopeId) => ranks.get(scopeId) ?? Infinity;
|
|
994
|
+
var coreBindingToMcpBinding = (binding) => McpSourceBindingRef.make({
|
|
995
|
+
sourceId: binding.sourceId,
|
|
996
|
+
sourceScopeId: binding.sourceScopeId,
|
|
997
|
+
scopeId: binding.scopeId,
|
|
998
|
+
slot: binding.slotKey,
|
|
999
|
+
value: binding.value,
|
|
1000
|
+
createdAt: binding.createdAt,
|
|
1001
|
+
updatedAt: binding.updatedAt
|
|
1002
|
+
});
|
|
1003
|
+
var listMcpSourceBindings = (ctx, sourceId, sourceScope) => Effect6.gen(function* () {
|
|
1004
|
+
const ranks = scopeRanks(ctx);
|
|
1005
|
+
const sourceSourceRank = scopeRank(ranks, sourceScope);
|
|
1006
|
+
if (sourceSourceRank === Infinity) return [];
|
|
1007
|
+
const bindings = yield* ctx.credentialBindings.listForSource({
|
|
1008
|
+
pluginId: MCP_PLUGIN_ID,
|
|
1009
|
+
sourceId,
|
|
1010
|
+
sourceScope: ScopeId.make(sourceScope)
|
|
1011
|
+
});
|
|
1012
|
+
return bindings.filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank).map(coreBindingToMcpBinding);
|
|
1013
|
+
});
|
|
1014
|
+
var resolveMcpSourceBinding = (ctx, sourceId, sourceScope, slot) => Effect6.gen(function* () {
|
|
1015
|
+
const ranks = scopeRanks(ctx);
|
|
1016
|
+
const sourceSourceRank = scopeRank(ranks, sourceScope);
|
|
1017
|
+
if (sourceSourceRank === Infinity) return null;
|
|
1018
|
+
const bindings = yield* ctx.credentialBindings.listForSource({
|
|
1019
|
+
pluginId: MCP_PLUGIN_ID,
|
|
1020
|
+
sourceId,
|
|
1021
|
+
sourceScope: ScopeId.make(sourceScope)
|
|
1022
|
+
});
|
|
1023
|
+
const binding = bindings.filter(
|
|
1024
|
+
(candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank
|
|
1025
|
+
).sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0];
|
|
1026
|
+
return binding ? coreBindingToMcpBinding(binding) : null;
|
|
1027
|
+
});
|
|
1028
|
+
var validateMcpBindingTarget = (ctx, input) => Effect6.gen(function* () {
|
|
1029
|
+
const ranks = scopeRanks(ctx);
|
|
1030
|
+
const sourceSourceRank = scopeRank(ranks, input.sourceScope);
|
|
1031
|
+
const targetRank = scopeRank(ranks, input.targetScope);
|
|
1032
|
+
const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`;
|
|
1033
|
+
if (sourceSourceRank === Infinity) {
|
|
1034
|
+
return yield* new StorageError({
|
|
1035
|
+
message: `MCP source binding references source scope "${input.sourceScope}" which is not in the executor's scope stack ${scopeList}.`,
|
|
1036
|
+
cause: void 0
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
if (targetRank === Infinity) {
|
|
1040
|
+
return yield* new StorageError({
|
|
1041
|
+
message: `MCP source binding targets scope "${input.targetScope}" which is not in the executor's scope stack ${scopeList}.`,
|
|
1042
|
+
cause: void 0
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (targetRank > sourceSourceRank) {
|
|
1046
|
+
return yield* new StorageError({
|
|
1047
|
+
message: `MCP source bindings for "${input.sourceId}" cannot be written at outer scope "${input.targetScope}" because the base source lives at "${input.sourceScope}"`,
|
|
1048
|
+
cause: void 0
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
var bindingTargetScope = (targetScope, bindings) => {
|
|
1053
|
+
if (bindings.length === 0) return Effect6.succeed(void 0);
|
|
1054
|
+
if (targetScope) return Effect6.succeed(targetScope);
|
|
1055
|
+
return Effect6.fail(
|
|
1056
|
+
new McpConnectionError({
|
|
1057
|
+
transport: "remote",
|
|
1058
|
+
message: "credentialTargetScope is required when adding direct MCP credentials"
|
|
1059
|
+
})
|
|
1060
|
+
);
|
|
1061
|
+
};
|
|
1062
|
+
var targetScopeForBinding = (fallbackTargetScope, binding) => {
|
|
1063
|
+
const targetScope = binding.targetScope ?? fallbackTargetScope;
|
|
1064
|
+
if (targetScope) return Effect6.succeed(targetScope);
|
|
1065
|
+
return Effect6.fail(
|
|
1066
|
+
new McpConnectionError({
|
|
1067
|
+
transport: "remote",
|
|
1068
|
+
message: "credentialTargetScope is required when adding direct MCP credentials"
|
|
1069
|
+
})
|
|
1070
|
+
);
|
|
1071
|
+
};
|
|
1072
|
+
var canonicalizeCredentialMap = (values, slotForName) => {
|
|
1073
|
+
const nextValues = {};
|
|
1074
|
+
const bindings = [];
|
|
1075
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
1076
|
+
if (typeof value === "string") {
|
|
1077
|
+
nextValues[name] = value;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if ("kind" in value) {
|
|
1081
|
+
nextValues[name] = value;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const slot = slotForName(name);
|
|
1085
|
+
nextValues[name] = ConfiguredCredentialBinding2.make({
|
|
1086
|
+
kind: "binding",
|
|
1087
|
+
slot,
|
|
1088
|
+
prefix: value.prefix
|
|
1089
|
+
});
|
|
1090
|
+
bindings.push({
|
|
1091
|
+
slot,
|
|
1092
|
+
targetScope: "targetScope" in value ? value.targetScope : void 0,
|
|
1093
|
+
value: {
|
|
1094
|
+
kind: "secret",
|
|
1095
|
+
secretId: SecretId.make(value.secretId),
|
|
1096
|
+
..."secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
return { values: nextValues, bindings };
|
|
1101
|
+
};
|
|
1102
|
+
var canonicalizeAuth = (auth) => {
|
|
1103
|
+
if (!auth || auth.kind === "none") return { auth: { kind: "none" }, bindings: [] };
|
|
1104
|
+
if (auth.kind === "header") {
|
|
1105
|
+
if ("secretSlot" in auth) return { auth, bindings: [] };
|
|
1106
|
+
return {
|
|
1107
|
+
auth: {
|
|
1108
|
+
kind: "header",
|
|
1109
|
+
headerName: auth.headerName,
|
|
1110
|
+
secretSlot: MCP_HEADER_AUTH_SLOT,
|
|
1111
|
+
prefix: auth.prefix
|
|
1112
|
+
},
|
|
1113
|
+
bindings: [
|
|
1114
|
+
{
|
|
1115
|
+
slot: MCP_HEADER_AUTH_SLOT,
|
|
1116
|
+
targetScope: auth.targetScope,
|
|
1117
|
+
value: {
|
|
1118
|
+
kind: "secret",
|
|
1119
|
+
secretId: SecretId.make(auth.secretId),
|
|
1120
|
+
...auth.secretScopeId ? { secretScopeId: auth.secretScopeId } : {}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
]
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
if ("connectionSlot" in auth) return { auth, bindings: [] };
|
|
1127
|
+
const bindings = [
|
|
1128
|
+
{
|
|
1129
|
+
slot: MCP_OAUTH_CONNECTION_SLOT,
|
|
1130
|
+
value: {
|
|
1131
|
+
kind: "connection",
|
|
1132
|
+
connectionId: ConnectionId.make(auth.connectionId)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
];
|
|
1136
|
+
if (auth.clientIdSecretId) {
|
|
1137
|
+
bindings.push({
|
|
1138
|
+
slot: MCP_OAUTH_CLIENT_ID_SLOT,
|
|
1139
|
+
value: { kind: "secret", secretId: SecretId.make(auth.clientIdSecretId) }
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (auth.clientSecretSecretId) {
|
|
1143
|
+
bindings.push({
|
|
1144
|
+
slot: MCP_OAUTH_CLIENT_SECRET_SLOT,
|
|
1145
|
+
value: { kind: "secret", secretId: SecretId.make(auth.clientSecretSecretId) }
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
auth: {
|
|
1150
|
+
kind: "oauth2",
|
|
1151
|
+
connectionSlot: MCP_OAUTH_CONNECTION_SLOT,
|
|
1152
|
+
...auth.clientIdSecretId ? { clientIdSlot: MCP_OAUTH_CLIENT_ID_SLOT } : {},
|
|
1153
|
+
...auth.clientSecretSecretId ? { clientSecretSlot: MCP_OAUTH_CLIENT_SECRET_SLOT } : {}
|
|
1154
|
+
},
|
|
1155
|
+
bindings
|
|
1156
|
+
};
|
|
1157
|
+
};
|
|
1158
|
+
var makeOAuthProvider = (accessToken) => ({
|
|
1159
|
+
get redirectUrl() {
|
|
1160
|
+
return "http://localhost/oauth/callback";
|
|
1161
|
+
},
|
|
1162
|
+
get clientMetadata() {
|
|
1163
|
+
return {
|
|
1164
|
+
redirect_uris: ["http://localhost/oauth/callback"],
|
|
1165
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1166
|
+
response_types: ["code"],
|
|
1167
|
+
token_endpoint_auth_method: "none",
|
|
1168
|
+
client_name: "Executor"
|
|
1169
|
+
};
|
|
1170
|
+
},
|
|
1171
|
+
clientInformation: () => void 0,
|
|
1172
|
+
saveClientInformation: () => void 0,
|
|
1173
|
+
tokens: () => ({ access_token: accessToken, token_type: "Bearer" }),
|
|
1174
|
+
saveTokens: () => void 0,
|
|
1175
|
+
redirectToAuthorization: async () => {
|
|
1176
|
+
throw new Error("MCP OAuth re-authorization required");
|
|
1177
|
+
},
|
|
1178
|
+
saveCodeVerifier: () => void 0,
|
|
1179
|
+
codeVerifier: () => {
|
|
1180
|
+
throw new Error("No active PKCE verifier");
|
|
1181
|
+
},
|
|
1182
|
+
saveDiscoveryState: () => void 0,
|
|
1183
|
+
discoveryState: () => void 0
|
|
1184
|
+
});
|
|
1185
|
+
var resolveSecretBackedMap = (values, ctx) => resolveSharedSecretBackedMap({
|
|
1186
|
+
values,
|
|
1187
|
+
getSecret: ctx.secrets.get,
|
|
1188
|
+
onMissing: (_name, value) => new McpConnectionError({
|
|
1189
|
+
transport: "remote",
|
|
1190
|
+
message: `Failed to resolve secret "${value.secretId}"`
|
|
1191
|
+
}),
|
|
1192
|
+
onError: (err, _name, value) => Predicate2.isTagged("SecretOwnedByConnectionError")(err) ? new McpConnectionError({
|
|
1193
|
+
transport: "remote",
|
|
1194
|
+
message: `Failed to resolve secret "${value.secretId}"`
|
|
1195
|
+
}) : err
|
|
1196
|
+
}).pipe(
|
|
1197
|
+
Effect6.mapError(
|
|
1198
|
+
(err) => Predicate2.isTagged("SecretOwnedByConnectionError")(err) ? new McpConnectionError({ transport: "remote", message: "Failed to resolve secret" }) : err
|
|
1199
|
+
)
|
|
1200
|
+
);
|
|
1201
|
+
var plainStringMap = (values) => {
|
|
1202
|
+
if (!values) return void 0;
|
|
1203
|
+
const entries = Object.entries(values).filter(
|
|
1204
|
+
(entry) => typeof entry[1] === "string"
|
|
1205
|
+
);
|
|
1206
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
1207
|
+
};
|
|
1208
|
+
var resolveMcpBindingValueMap = (ctx, values, params) => Effect6.gen(function* () {
|
|
1209
|
+
if (!values) return void 0;
|
|
1210
|
+
const resolved = {};
|
|
1211
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1212
|
+
if (typeof value === "string") {
|
|
1213
|
+
resolved[name] = value;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
const binding = yield* resolveMcpSourceBinding(
|
|
1217
|
+
ctx,
|
|
1218
|
+
params.sourceId,
|
|
1219
|
+
params.sourceScope,
|
|
1220
|
+
value.slot
|
|
1221
|
+
);
|
|
1222
|
+
if (binding?.value.kind === "secret") {
|
|
1223
|
+
const secret = yield* ctx.secrets.getAtScope(binding.value.secretId, binding.scopeId).pipe(
|
|
1224
|
+
Effect6.catchTag(
|
|
1225
|
+
"SecretOwnedByConnectionError",
|
|
1226
|
+
() => Effect6.fail(
|
|
1227
|
+
new McpConnectionError({
|
|
1228
|
+
transport: "remote",
|
|
1229
|
+
message: `Failed to resolve secret for ${params.missingLabel} "${name}"`
|
|
1230
|
+
})
|
|
1231
|
+
)
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
if (secret === null) {
|
|
1235
|
+
return yield* new McpConnectionError({
|
|
1236
|
+
transport: "remote",
|
|
1237
|
+
message: `Missing secret "${binding.value.secretId}" for ${params.missingLabel} "${name}"`
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (binding?.value.kind === "text") {
|
|
1244
|
+
resolved[name] = value.prefix ? `${value.prefix}${binding.value.text}` : binding.value.text;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
return yield* new McpConnectionError({
|
|
1248
|
+
transport: "remote",
|
|
1249
|
+
message: `Missing binding for ${params.missingLabel} "${name}"`
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
return Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
1253
|
+
});
|
|
1254
|
+
var resolveMcpCredentialInputMap = (ctx, values, params) => Effect6.gen(function* () {
|
|
1255
|
+
if (!values) return void 0;
|
|
1256
|
+
const resolved = {};
|
|
1257
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1258
|
+
if (typeof value === "string") {
|
|
1259
|
+
resolved[name] = value;
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if ("kind" in value) {
|
|
1263
|
+
const slotResolved = yield* resolveMcpBindingValueMap(
|
|
1264
|
+
ctx,
|
|
1265
|
+
{ [name]: value },
|
|
1266
|
+
{
|
|
1267
|
+
sourceId: params.sourceId,
|
|
1268
|
+
sourceScope: params.sourceScope,
|
|
1269
|
+
missingLabel: params.missingLabel
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1272
|
+
if (slotResolved?.[name] !== void 0) resolved[name] = slotResolved[name];
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const secretScope = "secretScopeId" in value ? value.secretScopeId ?? value.targetScope : params.targetScope ?? params.sourceScope;
|
|
1276
|
+
const secret = yield* ctx.secrets.getAtScope(SecretId.make(value.secretId), secretScope).pipe(
|
|
1277
|
+
Effect6.catchTag(
|
|
1278
|
+
"SecretOwnedByConnectionError",
|
|
1279
|
+
() => Effect6.fail(
|
|
1280
|
+
new McpConnectionError({
|
|
1281
|
+
transport: "remote",
|
|
1282
|
+
message: `Failed to resolve secret for ${params.missingLabel} "${name}"`
|
|
1283
|
+
})
|
|
1284
|
+
)
|
|
1285
|
+
)
|
|
1286
|
+
);
|
|
1287
|
+
if (secret === null) {
|
|
1288
|
+
return yield* new McpConnectionError({
|
|
1289
|
+
transport: "remote",
|
|
1290
|
+
message: `Missing secret "${value.secretId}" for ${params.missingLabel} "${name}"`
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
|
|
1294
|
+
}
|
|
1295
|
+
return Object.keys(resolved).length > 0 ? resolved : void 0;
|
|
1296
|
+
});
|
|
1297
|
+
var resolveMcpHeaderAuth = (ctx, sourceId, sourceScope, auth) => Effect6.gen(function* () {
|
|
1298
|
+
if (auth.kind !== "header") return {};
|
|
1299
|
+
const binding = yield* resolveMcpSourceBinding(ctx, sourceId, sourceScope, auth.secretSlot);
|
|
1300
|
+
if (binding?.value.kind === "secret") {
|
|
1301
|
+
const secret = yield* ctx.secrets.getAtScope(binding.value.secretId, binding.scopeId).pipe(
|
|
1302
|
+
Effect6.catchTag(
|
|
1303
|
+
"SecretOwnedByConnectionError",
|
|
1304
|
+
() => Effect6.fail(
|
|
1305
|
+
new McpConnectionError({
|
|
1306
|
+
transport: "remote",
|
|
1307
|
+
message: `Failed to resolve header auth binding "${auth.secretSlot}"`
|
|
1308
|
+
})
|
|
1309
|
+
)
|
|
1310
|
+
)
|
|
1311
|
+
);
|
|
1312
|
+
if (secret === null) {
|
|
1313
|
+
return yield* new McpConnectionError({
|
|
1314
|
+
transport: "remote",
|
|
1315
|
+
message: `Missing secret for header auth binding "${auth.secretSlot}"`
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
return { [auth.headerName]: auth.prefix ? `${auth.prefix}${secret}` : secret };
|
|
1319
|
+
}
|
|
1320
|
+
if (binding?.value.kind === "text") {
|
|
1321
|
+
return {
|
|
1322
|
+
[auth.headerName]: auth.prefix ? `${auth.prefix}${binding.value.text}` : binding.value.text
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
return yield* new McpConnectionError({
|
|
1326
|
+
transport: "remote",
|
|
1327
|
+
message: `Missing header auth binding "${auth.secretSlot}"`
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
var resolveMcpStoredOauthProvider = (ctx, sourceId, sourceScope, auth) => Effect6.gen(function* () {
|
|
1331
|
+
if (auth.kind !== "oauth2") return void 0;
|
|
1332
|
+
const binding = yield* resolveMcpSourceBinding(ctx, sourceId, sourceScope, auth.connectionSlot);
|
|
1333
|
+
if (binding?.value.kind !== "connection") {
|
|
1334
|
+
return yield* new McpConnectionError({
|
|
1335
|
+
transport: "remote",
|
|
1336
|
+
message: `Missing OAuth connection binding for MCP source "${sourceId}"`
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
const connectionId = binding.value.connectionId;
|
|
1340
|
+
const accessToken = yield* ctx.connections.accessTokenAtScope(connectionId, binding.scopeId).pipe(
|
|
1341
|
+
Effect6.mapError(
|
|
1342
|
+
({ message }) => new McpConnectionError({
|
|
1343
|
+
transport: "remote",
|
|
1344
|
+
message: `Failed to resolve OAuth connection "${connectionId}": ${message}`
|
|
1345
|
+
})
|
|
1346
|
+
)
|
|
1347
|
+
);
|
|
1348
|
+
return makeOAuthProvider(accessToken);
|
|
1349
|
+
});
|
|
1350
|
+
var resolveMcpInputAuth = (ctx, sourceId, sourceScope, targetScope, auth) => Effect6.gen(function* () {
|
|
1351
|
+
if (!auth || auth.kind === "none") return { headers: {} };
|
|
1352
|
+
if (auth.kind === "header") {
|
|
1353
|
+
if ("secretSlot" in auth) {
|
|
1354
|
+
const headers = yield* resolveMcpHeaderAuth(ctx, sourceId, sourceScope, auth);
|
|
1355
|
+
return { headers };
|
|
1356
|
+
}
|
|
1357
|
+
const secretScope = auth.secretScopeId ?? auth.targetScope ?? targetScope ?? sourceScope;
|
|
1358
|
+
const secret = yield* ctx.secrets.getAtScope(SecretId.make(auth.secretId), secretScope).pipe(
|
|
1359
|
+
Effect6.catchTag(
|
|
1360
|
+
"SecretOwnedByConnectionError",
|
|
1361
|
+
() => Effect6.fail(
|
|
1362
|
+
new McpConnectionError({
|
|
1363
|
+
transport: "remote",
|
|
1364
|
+
message: `Failed to resolve secret "${auth.secretId}"`
|
|
1365
|
+
})
|
|
1366
|
+
)
|
|
1367
|
+
)
|
|
1368
|
+
);
|
|
1369
|
+
if (secret === null) {
|
|
1370
|
+
return yield* new McpConnectionError({
|
|
1371
|
+
transport: "remote",
|
|
1372
|
+
message: `Failed to resolve secret "${auth.secretId}"`
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
headers: { [auth.headerName]: auth.prefix ? `${auth.prefix}${secret}` : secret }
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
const connection = "connectionId" in auth ? { id: ConnectionId.make(auth.connectionId), scope: targetScope ?? sourceScope } : yield* Effect6.gen(function* () {
|
|
1380
|
+
const binding = yield* resolveMcpSourceBinding(
|
|
1381
|
+
ctx,
|
|
1382
|
+
sourceId,
|
|
1383
|
+
sourceScope,
|
|
1384
|
+
auth.connectionSlot
|
|
1385
|
+
);
|
|
1386
|
+
return binding?.value.kind === "connection" ? { id: binding.value.connectionId, scope: binding.scopeId } : null;
|
|
1387
|
+
});
|
|
1388
|
+
if (connection === null) {
|
|
1389
|
+
return yield* new McpConnectionError({
|
|
1390
|
+
transport: "remote",
|
|
1391
|
+
message: `Missing OAuth connection binding for MCP source "${sourceId}"`
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
const accessToken = yield* ctx.connections.accessTokenAtScope(connection.id, connection.scope).pipe(
|
|
1395
|
+
Effect6.mapError(
|
|
1396
|
+
({ message }) => new McpConnectionError({
|
|
1397
|
+
transport: "remote",
|
|
1398
|
+
message: `Failed to resolve OAuth connection "${connection.id}": ${message}`
|
|
1399
|
+
})
|
|
1400
|
+
)
|
|
1401
|
+
);
|
|
1402
|
+
return { headers: {}, authProvider: makeOAuthProvider(accessToken) };
|
|
1403
|
+
});
|
|
1404
|
+
var resolveConnectorInput = (sourceId, sourceScope, sd, ctx, allowStdio) => {
|
|
1405
|
+
if (sd.transport === "stdio") {
|
|
1406
|
+
if (!allowStdio) {
|
|
1407
|
+
return Effect6.fail(
|
|
1408
|
+
new McpConnectionError({
|
|
1409
|
+
transport: "stdio",
|
|
1410
|
+
message: "MCP stdio transport is disabled. Enable it by passing `dangerouslyAllowStdioMCP: true` to mcpPlugin() \u2014 only safe for trusted local contexts."
|
|
1411
|
+
})
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
return Effect6.succeed({
|
|
1415
|
+
transport: "stdio",
|
|
1416
|
+
command: sd.command,
|
|
1417
|
+
args: sd.args,
|
|
1418
|
+
env: sd.env,
|
|
1419
|
+
cwd: sd.cwd
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
return Effect6.gen(function* () {
|
|
1423
|
+
const resolvedHeaders = yield* resolveMcpBindingValueMap(ctx, sd.headers, {
|
|
1424
|
+
sourceId,
|
|
1425
|
+
sourceScope,
|
|
1426
|
+
missingLabel: "header"
|
|
1427
|
+
});
|
|
1428
|
+
const resolvedQueryParams = yield* resolveMcpBindingValueMap(ctx, sd.queryParams, {
|
|
1429
|
+
sourceId,
|
|
1430
|
+
sourceScope,
|
|
1431
|
+
missingLabel: "query parameter"
|
|
1432
|
+
});
|
|
1433
|
+
const headers = { ...resolvedHeaders ?? {} };
|
|
1434
|
+
const auth = sd.auth;
|
|
1435
|
+
if (auth.kind === "header") {
|
|
1436
|
+
Object.assign(headers, yield* resolveMcpHeaderAuth(ctx, sourceId, sourceScope, auth));
|
|
1437
|
+
}
|
|
1438
|
+
const authProvider = yield* resolveMcpStoredOauthProvider(ctx, sourceId, sourceScope, auth);
|
|
1439
|
+
return {
|
|
1440
|
+
transport: "remote",
|
|
1441
|
+
endpoint: sd.endpoint,
|
|
1442
|
+
remoteTransport: sd.remoteTransport,
|
|
1443
|
+
queryParams: resolvedQueryParams,
|
|
1444
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
1445
|
+
authProvider
|
|
1446
|
+
};
|
|
1447
|
+
});
|
|
1448
|
+
};
|
|
1449
|
+
var makeRuntime = () => Effect6.gen(function* () {
|
|
1450
|
+
const cacheScope = yield* Scope.make();
|
|
1451
|
+
const pendingConnectors = /* @__PURE__ */ new Map();
|
|
1452
|
+
const connectionCache = yield* ScopedCache2.make({
|
|
1453
|
+
lookup: (key) => Effect6.acquireRelease(
|
|
1454
|
+
Effect6.suspend(() => {
|
|
1455
|
+
const connector = pendingConnectors.get(key);
|
|
1456
|
+
if (!connector) {
|
|
1457
|
+
return Effect6.fail(
|
|
1458
|
+
new McpConnectionError({
|
|
1459
|
+
transport: "auto",
|
|
1460
|
+
message: `No pending connector for key: ${key}`
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
return connector;
|
|
1465
|
+
}),
|
|
1466
|
+
(connection) => Effect6.ignore(
|
|
1467
|
+
Effect6.tryPromise({
|
|
1468
|
+
try: () => connection.close(),
|
|
1469
|
+
catch: () => new McpConnectionError({
|
|
1470
|
+
transport: "auto",
|
|
1471
|
+
message: "Failed to close MCP connection"
|
|
1472
|
+
})
|
|
1473
|
+
})
|
|
1474
|
+
)
|
|
1475
|
+
),
|
|
1476
|
+
capacity: 64,
|
|
1477
|
+
timeToLive: Duration2.minutes(5)
|
|
1478
|
+
}).pipe(Scope.provide(cacheScope));
|
|
1479
|
+
return { connectionCache, pendingConnectors, cacheScope };
|
|
1480
|
+
});
|
|
1481
|
+
var secretRef = (id) => `${SECRET_REF_PREFIX}${id}`;
|
|
1482
|
+
var authToConfig = (auth) => {
|
|
1483
|
+
if (!auth) return void 0;
|
|
1484
|
+
if (auth.kind === "none") return { kind: "none" };
|
|
1485
|
+
if (auth.kind === "header") {
|
|
1486
|
+
if (!("secretId" in auth)) return void 0;
|
|
1487
|
+
return {
|
|
1488
|
+
kind: "header",
|
|
1489
|
+
headerName: auth.headerName,
|
|
1490
|
+
secret: secretRef(auth.secretId),
|
|
1491
|
+
prefix: auth.prefix
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if (!("connectionId" in auth)) return void 0;
|
|
1495
|
+
return {
|
|
1496
|
+
kind: "oauth2",
|
|
1497
|
+
connectionId: auth.connectionId
|
|
1498
|
+
};
|
|
1499
|
+
};
|
|
1500
|
+
var toCredentialInput = (bySlot, configured) => {
|
|
1501
|
+
if (typeof configured === "string") return configured;
|
|
1502
|
+
const value = bySlot.get(configured.slot);
|
|
1503
|
+
if (!value) return void 0;
|
|
1504
|
+
if (value.kind === "secret") {
|
|
1505
|
+
return {
|
|
1506
|
+
secretId: value.secretId,
|
|
1507
|
+
...configured.prefix ? { prefix: configured.prefix } : {}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (value.kind === "text") return value.text;
|
|
1511
|
+
return void 0;
|
|
1512
|
+
};
|
|
1513
|
+
var toCredentialInputMap = (bySlot, values) => {
|
|
1514
|
+
if (!values) return void 0;
|
|
1515
|
+
const out = {};
|
|
1516
|
+
for (const [name, configured] of Object.entries(values)) {
|
|
1517
|
+
const input = toCredentialInput(bySlot, configured);
|
|
1518
|
+
if (input !== void 0) out[name] = input;
|
|
1519
|
+
}
|
|
1520
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1521
|
+
};
|
|
1522
|
+
var toAuthInput = (bySlot, auth) => {
|
|
1523
|
+
if (auth.kind === "none") return { kind: "none" };
|
|
1524
|
+
if (auth.kind === "header") {
|
|
1525
|
+
const value = bySlot.get(auth.secretSlot);
|
|
1526
|
+
if (value?.kind !== "secret") return void 0;
|
|
1527
|
+
return {
|
|
1528
|
+
kind: "header",
|
|
1529
|
+
headerName: auth.headerName,
|
|
1530
|
+
secretId: value.secretId,
|
|
1531
|
+
prefix: auth.prefix
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
const connection = bySlot.get(auth.connectionSlot);
|
|
1535
|
+
if (connection?.kind !== "connection") return void 0;
|
|
1536
|
+
return { kind: "oauth2", connectionId: connection.connectionId };
|
|
1537
|
+
};
|
|
1538
|
+
var inputFormFromStored = (bindings, stored, scope, sourceName, namespace) => {
|
|
1539
|
+
if (stored.transport === "stdio") {
|
|
1540
|
+
return {
|
|
1541
|
+
transport: "stdio",
|
|
1542
|
+
scope,
|
|
1543
|
+
name: sourceName,
|
|
1544
|
+
namespace,
|
|
1545
|
+
command: stored.command,
|
|
1546
|
+
args: stored.args ? [...stored.args] : void 0,
|
|
1547
|
+
env: stored.env,
|
|
1548
|
+
cwd: stored.cwd
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const bySlot = new Map(bindings.map((b) => [b.slotKey, b.value]));
|
|
1552
|
+
return {
|
|
1553
|
+
transport: "remote",
|
|
1554
|
+
scope,
|
|
1555
|
+
name: sourceName,
|
|
1556
|
+
namespace,
|
|
1557
|
+
endpoint: stored.endpoint,
|
|
1558
|
+
remoteTransport: stored.remoteTransport,
|
|
1559
|
+
headers: toCredentialInputMap(bySlot, stored.headers),
|
|
1560
|
+
queryParams: toCredentialInputMap(bySlot, stored.queryParams),
|
|
1561
|
+
auth: toAuthInput(bySlot, stored.auth)
|
|
1562
|
+
};
|
|
1563
|
+
};
|
|
1564
|
+
var toMcpConfigEntry = (namespace, sourceName, config) => {
|
|
1565
|
+
if (config.transport === "stdio") {
|
|
1566
|
+
const entry2 = {
|
|
1567
|
+
kind: "mcp",
|
|
1568
|
+
transport: "stdio",
|
|
1569
|
+
name: sourceName,
|
|
1570
|
+
command: config.command,
|
|
1571
|
+
args: config.args,
|
|
1572
|
+
env: config.env,
|
|
1573
|
+
cwd: config.cwd,
|
|
1574
|
+
namespace
|
|
1575
|
+
};
|
|
1576
|
+
return entry2;
|
|
1577
|
+
}
|
|
1578
|
+
const entry = {
|
|
1579
|
+
kind: "mcp",
|
|
1580
|
+
transport: "remote",
|
|
1581
|
+
name: sourceName,
|
|
1582
|
+
endpoint: config.endpoint,
|
|
1583
|
+
remoteTransport: config.remoteTransport,
|
|
1584
|
+
queryParams: plainStringMap(config.queryParams),
|
|
1585
|
+
headers: plainStringMap(config.headers),
|
|
1586
|
+
namespace,
|
|
1587
|
+
auth: authToConfig(config.auth)
|
|
1588
|
+
};
|
|
1589
|
+
return entry;
|
|
1590
|
+
};
|
|
1591
|
+
var mcpPlugin = definePlugin((options) => {
|
|
1592
|
+
const allowStdio = options?.dangerouslyAllowStdioMCP ?? false;
|
|
1593
|
+
const runtimeRef = { current: null };
|
|
1594
|
+
const ensureRuntime = () => runtimeRef.current ? Effect6.succeed(runtimeRef.current) : makeRuntime().pipe(
|
|
1595
|
+
Effect6.tap(
|
|
1596
|
+
(rt) => Effect6.sync(() => {
|
|
1597
|
+
runtimeRef.current = rt;
|
|
1598
|
+
})
|
|
1599
|
+
)
|
|
1600
|
+
);
|
|
1601
|
+
return {
|
|
1602
|
+
id: "mcp",
|
|
1603
|
+
packageName: "@executor-js/plugin-mcp",
|
|
1604
|
+
// Surfaced to the client bundle via the Vite plugin (see
|
|
1605
|
+
// `@executor-js/vite-plugin`). The MCP `./client` factory reads
|
|
1606
|
+
// `allowStdio` and gates the stdio tab + presets in AddMcpSource —
|
|
1607
|
+
// so the server's `dangerouslyAllowStdioMCP` flag is the single
|
|
1608
|
+
// source of truth for both runtime and UI.
|
|
1609
|
+
clientConfig: { allowStdio },
|
|
1610
|
+
schema: mcpSchema,
|
|
1611
|
+
storage: (deps) => makeMcpStore(deps),
|
|
1612
|
+
extension: (ctx) => {
|
|
1613
|
+
const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
|
|
1614
|
+
const probeEndpoint = (input) => Effect6.gen(function* () {
|
|
1615
|
+
const endpoint = typeof input === "string" ? input : input.endpoint;
|
|
1616
|
+
const trimmed = endpoint.trim();
|
|
1617
|
+
if (!trimmed) {
|
|
1618
|
+
return yield* new McpConnectionError({
|
|
1619
|
+
transport: "remote",
|
|
1620
|
+
message: "Endpoint URL is required"
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
const name = yield* Effect6.try({
|
|
1624
|
+
try: () => new URL(trimmed).hostname,
|
|
1625
|
+
catch: () => "mcp"
|
|
1626
|
+
}).pipe(Effect6.orElseSucceed(() => "mcp"));
|
|
1627
|
+
const namespace = deriveMcpNamespace({ endpoint: trimmed });
|
|
1628
|
+
const probeHeaders = typeof input === "string" ? void 0 : yield* resolveSecretBackedMap(input.headers, ctx);
|
|
1629
|
+
const probeQueryParams = typeof input === "string" ? void 0 : yield* resolveSecretBackedMap(input.queryParams, ctx);
|
|
1630
|
+
const connector = createMcpConnector({
|
|
1631
|
+
transport: "remote",
|
|
1632
|
+
endpoint: trimmed,
|
|
1633
|
+
headers: probeHeaders,
|
|
1634
|
+
queryParams: probeQueryParams
|
|
1635
|
+
});
|
|
1636
|
+
const result = yield* discoverTools(connector).pipe(
|
|
1637
|
+
Effect6.map((m) => ({ ok: true, manifest: m })),
|
|
1638
|
+
Effect6.catch(() => Effect6.succeed({ ok: false, manifest: null })),
|
|
1639
|
+
Effect6.withSpan("mcp.plugin.discover_tools")
|
|
1640
|
+
);
|
|
1641
|
+
if (result.ok && result.manifest) {
|
|
1642
|
+
return {
|
|
1643
|
+
connected: true,
|
|
1644
|
+
requiresOAuth: false,
|
|
1645
|
+
supportsDynamicRegistration: false,
|
|
1646
|
+
name: result.manifest.server?.name ?? name,
|
|
1647
|
+
namespace,
|
|
1648
|
+
toolCount: result.manifest.tools.length,
|
|
1649
|
+
serverName: result.manifest.server?.name ?? null
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const shape = yield* probeMcpEndpointShape(trimmed, {
|
|
1653
|
+
httpClientLayer,
|
|
1654
|
+
headers: probeHeaders,
|
|
1655
|
+
queryParams: probeQueryParams
|
|
1656
|
+
});
|
|
1657
|
+
if (shape.kind !== "mcp") {
|
|
1658
|
+
return yield* new McpConnectionError({
|
|
1659
|
+
transport: "remote",
|
|
1660
|
+
message: userFacingProbeMessage(shape)
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
const probeResult = yield* ctx.oauth.probe({
|
|
1664
|
+
endpoint: trimmed,
|
|
1665
|
+
headers: probeHeaders,
|
|
1666
|
+
queryParams: probeQueryParams
|
|
1667
|
+
}).pipe(
|
|
1668
|
+
Effect6.map((oauth) => ({ ok: true, oauth })),
|
|
1669
|
+
Effect6.catch(() => Effect6.succeed({ ok: false, oauth: null })),
|
|
1670
|
+
Effect6.withSpan("mcp.plugin.probe_oauth")
|
|
1671
|
+
);
|
|
1672
|
+
if (probeResult.ok) {
|
|
1673
|
+
return {
|
|
1674
|
+
connected: false,
|
|
1675
|
+
requiresOAuth: true,
|
|
1676
|
+
supportsDynamicRegistration: probeResult.oauth.supportsDynamicRegistration,
|
|
1677
|
+
name,
|
|
1678
|
+
namespace,
|
|
1679
|
+
toolCount: null,
|
|
1680
|
+
serverName: null
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
return yield* new McpConnectionError({
|
|
1684
|
+
transport: "remote",
|
|
1685
|
+
message: "This server requires authentication, but OAuth metadata wasn't found. Add credentials (Authorization header, query parameter, or API key) below and retry."
|
|
1686
|
+
});
|
|
1687
|
+
}).pipe(
|
|
1688
|
+
Effect6.withSpan("mcp.plugin.probe_endpoint", {
|
|
1689
|
+
attributes: { "mcp.endpoint": typeof input === "string" ? input : input.endpoint }
|
|
1690
|
+
})
|
|
1691
|
+
);
|
|
1692
|
+
const configFile = options?.configFile;
|
|
1693
|
+
const addSource = (config) => Effect6.gen(function* () {
|
|
1694
|
+
const namespace = normalizeNamespace(config);
|
|
1695
|
+
const canonicalRemote = config.transport === "remote" ? {
|
|
1696
|
+
headers: canonicalizeCredentialMap(config.headers, mcpHeaderSlot),
|
|
1697
|
+
queryParams: canonicalizeCredentialMap(config.queryParams, mcpQueryParamSlot),
|
|
1698
|
+
auth: canonicalizeAuth(config.auth)
|
|
1699
|
+
} : null;
|
|
1700
|
+
const directBindings = canonicalRemote ? [
|
|
1701
|
+
...canonicalRemote.headers.bindings,
|
|
1702
|
+
...canonicalRemote.queryParams.bindings,
|
|
1703
|
+
...canonicalRemote.auth.bindings
|
|
1704
|
+
] : [];
|
|
1705
|
+
for (const binding of directBindings) {
|
|
1706
|
+
const bindingTargetScope2 = yield* targetScopeForBinding(
|
|
1707
|
+
config.transport === "remote" ? config.credentialTargetScope : void 0,
|
|
1708
|
+
binding
|
|
1709
|
+
);
|
|
1710
|
+
yield* validateMcpBindingTarget(ctx, {
|
|
1711
|
+
sourceId: namespace,
|
|
1712
|
+
sourceScope: config.scope,
|
|
1713
|
+
targetScope: bindingTargetScope2
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
const targetScope = config.transport === "remote" && directBindings[0] ? yield* targetScopeForBinding(config.credentialTargetScope, directBindings[0]) : void 0;
|
|
1717
|
+
const sd = toStoredSourceData(
|
|
1718
|
+
config,
|
|
1719
|
+
canonicalRemote ? {
|
|
1720
|
+
headers: canonicalRemote.headers.values,
|
|
1721
|
+
queryParams: canonicalRemote.queryParams.values,
|
|
1722
|
+
auth: canonicalRemote.auth.auth
|
|
1723
|
+
} : void 0
|
|
1724
|
+
);
|
|
1725
|
+
const resolved = yield* (config.transport === "remote" ? Effect6.gen(function* () {
|
|
1726
|
+
const resolvedHeaders = yield* resolveMcpCredentialInputMap(ctx, config.headers, {
|
|
1727
|
+
sourceId: namespace,
|
|
1728
|
+
sourceScope: config.scope,
|
|
1729
|
+
targetScope,
|
|
1730
|
+
missingLabel: "header"
|
|
1731
|
+
});
|
|
1732
|
+
const resolvedQueryParams = yield* resolveMcpCredentialInputMap(
|
|
1733
|
+
ctx,
|
|
1734
|
+
config.queryParams,
|
|
1735
|
+
{
|
|
1736
|
+
sourceId: namespace,
|
|
1737
|
+
sourceScope: config.scope,
|
|
1738
|
+
targetScope,
|
|
1739
|
+
missingLabel: "query parameter"
|
|
1740
|
+
}
|
|
1741
|
+
);
|
|
1742
|
+
const resolvedAuth = yield* resolveMcpInputAuth(
|
|
1743
|
+
ctx,
|
|
1744
|
+
namespace,
|
|
1745
|
+
config.scope,
|
|
1746
|
+
targetScope,
|
|
1747
|
+
config.auth
|
|
1748
|
+
);
|
|
1749
|
+
const headers = {
|
|
1750
|
+
...resolvedHeaders ?? {},
|
|
1751
|
+
...resolvedAuth.headers
|
|
1752
|
+
};
|
|
1753
|
+
return {
|
|
1754
|
+
transport: "remote",
|
|
1755
|
+
endpoint: config.endpoint,
|
|
1756
|
+
remoteTransport: config.remoteTransport ?? "auto",
|
|
1757
|
+
queryParams: resolvedQueryParams,
|
|
1758
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
1759
|
+
authProvider: resolvedAuth.authProvider
|
|
1760
|
+
};
|
|
1761
|
+
}) : resolveConnectorInput(namespace, config.scope, sd, ctx, allowStdio)).pipe(
|
|
1762
|
+
Effect6.result,
|
|
1763
|
+
Effect6.withSpan("mcp.plugin.resolve_connector", {
|
|
1764
|
+
attributes: {
|
|
1765
|
+
"mcp.source.namespace": namespace,
|
|
1766
|
+
"mcp.source.transport": sd.transport
|
|
1767
|
+
}
|
|
1768
|
+
})
|
|
1769
|
+
);
|
|
1770
|
+
if (Result.isFailure(resolved) && sd.transport === "stdio") {
|
|
1771
|
+
return yield* Effect6.fail(resolved.failure);
|
|
1772
|
+
}
|
|
1773
|
+
const discovery = Result.isSuccess(resolved) ? yield* discoverTools(createMcpConnector(resolved.success)).pipe(
|
|
1774
|
+
Effect6.mapError(
|
|
1775
|
+
({ message }) => new McpToolDiscoveryError({
|
|
1776
|
+
stage: "list_tools",
|
|
1777
|
+
message: `MCP discovery failed: ${message}`
|
|
1778
|
+
})
|
|
1779
|
+
),
|
|
1780
|
+
Effect6.result,
|
|
1781
|
+
Effect6.withSpan("mcp.plugin.discover_tools", {
|
|
1782
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1783
|
+
})
|
|
1784
|
+
) : Result.fail(resolved.failure);
|
|
1785
|
+
const manifest = Result.isSuccess(discovery) ? discovery.success : { server: void 0, tools: [] };
|
|
1786
|
+
const sourceName = config.name ?? manifest.server?.name ?? namespace;
|
|
1787
|
+
yield* ctx.transaction(
|
|
1788
|
+
Effect6.gen(function* () {
|
|
1789
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, config.scope);
|
|
1790
|
+
yield* ctx.storage.removeSource(namespace, config.scope);
|
|
1791
|
+
yield* ctx.storage.putSource({
|
|
1792
|
+
namespace,
|
|
1793
|
+
scope: config.scope,
|
|
1794
|
+
name: sourceName,
|
|
1795
|
+
config: sd
|
|
1796
|
+
});
|
|
1797
|
+
yield* ctx.storage.putBindings(
|
|
1798
|
+
namespace,
|
|
1799
|
+
config.scope,
|
|
1800
|
+
manifest.tools.map((e) => ({
|
|
1801
|
+
toolId: `${namespace}.${e.toolId}`,
|
|
1802
|
+
binding: toBinding(e)
|
|
1803
|
+
}))
|
|
1804
|
+
);
|
|
1805
|
+
yield* ctx.core.sources.register({
|
|
1806
|
+
id: namespace,
|
|
1807
|
+
scope: config.scope,
|
|
1808
|
+
kind: "mcp",
|
|
1809
|
+
name: sourceName,
|
|
1810
|
+
url: sd.transport === "remote" ? sd.endpoint : void 0,
|
|
1811
|
+
canRemove: true,
|
|
1812
|
+
canRefresh: true,
|
|
1813
|
+
canEdit: sd.transport === "remote",
|
|
1814
|
+
tools: manifest.tools.map((e) => ({
|
|
1815
|
+
name: e.toolId,
|
|
1816
|
+
description: e.description ?? `MCP tool: ${e.toolName}`,
|
|
1817
|
+
inputSchema: e.inputSchema,
|
|
1818
|
+
outputSchema: e.outputSchema
|
|
1819
|
+
}))
|
|
1820
|
+
});
|
|
1821
|
+
if (directBindings.length > 0) {
|
|
1822
|
+
for (const binding of directBindings) {
|
|
1823
|
+
const bindingTargetScope2 = yield* targetScopeForBinding(
|
|
1824
|
+
config.transport === "remote" ? config.credentialTargetScope : void 0,
|
|
1825
|
+
binding
|
|
1826
|
+
);
|
|
1827
|
+
yield* ctx.credentialBindings.set({
|
|
1828
|
+
targetScope: ScopeId.make(bindingTargetScope2),
|
|
1829
|
+
pluginId: MCP_PLUGIN_ID,
|
|
1830
|
+
sourceId: namespace,
|
|
1831
|
+
sourceScope: ScopeId.make(config.scope),
|
|
1832
|
+
slotKey: binding.slot,
|
|
1833
|
+
value: binding.value
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
})
|
|
1838
|
+
).pipe(
|
|
1839
|
+
Effect6.withSpan("mcp.plugin.persist_source", {
|
|
1840
|
+
attributes: {
|
|
1841
|
+
"mcp.source.namespace": namespace,
|
|
1842
|
+
"mcp.source.tool_count": manifest.tools.length
|
|
1843
|
+
}
|
|
1844
|
+
})
|
|
1845
|
+
);
|
|
1846
|
+
if (configFile) {
|
|
1847
|
+
yield* configFile.upsertSource(toMcpConfigEntry(namespace, sourceName, config)).pipe(Effect6.withSpan("mcp.plugin.config_file.upsert"));
|
|
1848
|
+
}
|
|
1849
|
+
if (Result.isFailure(discovery)) {
|
|
1850
|
+
return yield* Effect6.fail(discovery.failure);
|
|
1851
|
+
}
|
|
1852
|
+
return { toolCount: manifest.tools.length, namespace };
|
|
1853
|
+
}).pipe(
|
|
1854
|
+
Effect6.withSpan("mcp.plugin.add_source", {
|
|
1855
|
+
attributes: {
|
|
1856
|
+
"mcp.source.transport": config.transport,
|
|
1857
|
+
"mcp.source.name": config.name
|
|
1858
|
+
}
|
|
1859
|
+
})
|
|
1860
|
+
);
|
|
1861
|
+
const removeSource = (namespace, scope) => Effect6.gen(function* () {
|
|
1862
|
+
yield* ctx.transaction(
|
|
1863
|
+
Effect6.gen(function* () {
|
|
1864
|
+
yield* ctx.credentialBindings.removeForSource({
|
|
1865
|
+
pluginId: MCP_PLUGIN_ID,
|
|
1866
|
+
sourceId: namespace,
|
|
1867
|
+
sourceScope: ScopeId.make(scope)
|
|
1868
|
+
});
|
|
1869
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, scope);
|
|
1870
|
+
yield* ctx.storage.removeSource(namespace, scope);
|
|
1871
|
+
yield* ctx.core.sources.unregister({ id: namespace, targetScope: scope });
|
|
1872
|
+
})
|
|
1873
|
+
).pipe(Effect6.withSpan("mcp.plugin.persist_remove"));
|
|
1874
|
+
if (configFile) {
|
|
1875
|
+
yield* configFile.removeSource(namespace).pipe(Effect6.withSpan("mcp.plugin.config_file.remove"));
|
|
1876
|
+
}
|
|
1877
|
+
}).pipe(
|
|
1878
|
+
Effect6.withSpan("mcp.plugin.remove_source", {
|
|
1879
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1880
|
+
})
|
|
1881
|
+
);
|
|
1882
|
+
const refreshSource = (namespace, scope) => Effect6.gen(function* () {
|
|
1883
|
+
const sd = yield* ctx.storage.getSourceConfig(namespace, scope).pipe(
|
|
1884
|
+
Effect6.withSpan("mcp.plugin.load_source_config", {
|
|
1885
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1886
|
+
})
|
|
1887
|
+
);
|
|
1888
|
+
if (!sd) {
|
|
1889
|
+
return yield* new McpConnectionError({
|
|
1890
|
+
transport: "remote",
|
|
1891
|
+
message: `No stored config for MCP source "${namespace}"`
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const ci = yield* resolveConnectorInput(namespace, scope, sd, ctx, allowStdio).pipe(
|
|
1895
|
+
Effect6.withSpan("mcp.plugin.resolve_connector", {
|
|
1896
|
+
attributes: {
|
|
1897
|
+
"mcp.source.namespace": namespace,
|
|
1898
|
+
"mcp.source.transport": sd.transport
|
|
1899
|
+
}
|
|
1900
|
+
})
|
|
1901
|
+
);
|
|
1902
|
+
const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(
|
|
1903
|
+
Effect6.mapError(
|
|
1904
|
+
({ message }) => new McpToolDiscoveryError({
|
|
1905
|
+
stage: "list_tools",
|
|
1906
|
+
message: `MCP refresh failed: ${message}`
|
|
1907
|
+
})
|
|
1908
|
+
),
|
|
1909
|
+
Effect6.withSpan("mcp.plugin.discover_tools", {
|
|
1910
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
1913
|
+
const existing = yield* ctx.storage.getSource(namespace, scope);
|
|
1914
|
+
const sourceName = manifest.server?.name ?? existing?.name ?? namespace;
|
|
1915
|
+
yield* ctx.transaction(
|
|
1916
|
+
Effect6.gen(function* () {
|
|
1917
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, scope);
|
|
1918
|
+
yield* ctx.core.sources.unregister({ id: namespace, targetScope: scope });
|
|
1919
|
+
yield* ctx.storage.putBindings(
|
|
1920
|
+
namespace,
|
|
1921
|
+
scope,
|
|
1922
|
+
manifest.tools.map((e) => ({
|
|
1923
|
+
toolId: `${namespace}.${e.toolId}`,
|
|
1924
|
+
binding: toBinding(e)
|
|
1925
|
+
}))
|
|
1926
|
+
);
|
|
1927
|
+
yield* ctx.core.sources.register({
|
|
1928
|
+
id: namespace,
|
|
1929
|
+
scope,
|
|
1930
|
+
kind: "mcp",
|
|
1931
|
+
name: sourceName,
|
|
1932
|
+
url: sd.transport === "remote" ? sd.endpoint : void 0,
|
|
1933
|
+
canRemove: true,
|
|
1934
|
+
canRefresh: true,
|
|
1935
|
+
canEdit: sd.transport === "remote",
|
|
1936
|
+
tools: manifest.tools.map((e) => ({
|
|
1937
|
+
name: e.toolId,
|
|
1938
|
+
description: e.description ?? `MCP tool: ${e.toolName}`,
|
|
1939
|
+
inputSchema: e.inputSchema,
|
|
1940
|
+
outputSchema: e.outputSchema
|
|
1941
|
+
}))
|
|
1942
|
+
});
|
|
1943
|
+
})
|
|
1944
|
+
).pipe(
|
|
1945
|
+
Effect6.withSpan("mcp.plugin.persist_source", {
|
|
1946
|
+
attributes: {
|
|
1947
|
+
"mcp.source.namespace": namespace,
|
|
1948
|
+
"mcp.source.tool_count": manifest.tools.length
|
|
1949
|
+
}
|
|
1950
|
+
})
|
|
1951
|
+
);
|
|
1952
|
+
return { toolCount: manifest.tools.length };
|
|
1953
|
+
}).pipe(
|
|
1954
|
+
Effect6.withSpan("mcp.plugin.refresh_source", {
|
|
1955
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1956
|
+
})
|
|
1957
|
+
);
|
|
1958
|
+
const updateSource = (namespace, scope, input) => Effect6.gen(function* () {
|
|
1959
|
+
const existing = yield* ctx.storage.getSource(namespace, scope);
|
|
1960
|
+
if (!existing || existing.config.transport !== "remote") return;
|
|
1961
|
+
const canonicalHeaders = input.headers !== void 0 ? canonicalizeCredentialMap(input.headers, mcpHeaderSlot) : null;
|
|
1962
|
+
const canonicalQueryParams = input.queryParams !== void 0 ? canonicalizeCredentialMap(input.queryParams, mcpQueryParamSlot) : null;
|
|
1963
|
+
const canonicalAuth = input.auth !== void 0 ? canonicalizeAuth(input.auth) : null;
|
|
1964
|
+
const directBindings = [
|
|
1965
|
+
...canonicalHeaders?.bindings ?? [],
|
|
1966
|
+
...canonicalQueryParams?.bindings ?? [],
|
|
1967
|
+
...canonicalAuth?.bindings ?? []
|
|
1968
|
+
];
|
|
1969
|
+
const targetScope = yield* bindingTargetScope(
|
|
1970
|
+
input.credentialTargetScope,
|
|
1971
|
+
directBindings
|
|
1972
|
+
);
|
|
1973
|
+
if (targetScope) {
|
|
1974
|
+
yield* validateMcpBindingTarget(ctx, {
|
|
1975
|
+
sourceId: namespace,
|
|
1976
|
+
sourceScope: scope,
|
|
1977
|
+
targetScope
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
const remote = existing.config;
|
|
1981
|
+
const updatedConfig = {
|
|
1982
|
+
...remote,
|
|
1983
|
+
...input.endpoint !== void 0 ? { endpoint: input.endpoint } : {},
|
|
1984
|
+
...canonicalHeaders ? { headers: canonicalHeaders.values } : {},
|
|
1985
|
+
...canonicalAuth ? { auth: canonicalAuth.auth } : {},
|
|
1986
|
+
...canonicalQueryParams ? { queryParams: canonicalQueryParams.values } : {}
|
|
1987
|
+
};
|
|
1988
|
+
const sourceName = input.name?.trim() || existing.name;
|
|
1989
|
+
const affectedPrefixes = [
|
|
1990
|
+
...input.headers !== void 0 ? ["header:"] : [],
|
|
1991
|
+
...input.queryParams !== void 0 ? ["query_param:"] : [],
|
|
1992
|
+
...input.auth !== void 0 ? ["auth:"] : []
|
|
1993
|
+
];
|
|
1994
|
+
const replacementTargetScope = targetScope ?? input.credentialTargetScope ?? scope;
|
|
1995
|
+
yield* ctx.transaction(
|
|
1996
|
+
Effect6.gen(function* () {
|
|
1997
|
+
yield* ctx.storage.putSource({
|
|
1998
|
+
namespace,
|
|
1999
|
+
scope,
|
|
2000
|
+
name: sourceName,
|
|
2001
|
+
config: updatedConfig
|
|
2002
|
+
});
|
|
2003
|
+
if (affectedPrefixes.length > 0 || directBindings.length > 0) {
|
|
2004
|
+
yield* ctx.credentialBindings.replaceForSource({
|
|
2005
|
+
targetScope: ScopeId.make(replacementTargetScope),
|
|
2006
|
+
pluginId: MCP_PLUGIN_ID,
|
|
2007
|
+
sourceId: namespace,
|
|
2008
|
+
sourceScope: ScopeId.make(scope),
|
|
2009
|
+
slotPrefixes: affectedPrefixes,
|
|
2010
|
+
bindings: directBindings.map((binding) => ({
|
|
2011
|
+
slotKey: binding.slot,
|
|
2012
|
+
value: binding.value
|
|
2013
|
+
}))
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
})
|
|
2017
|
+
);
|
|
2018
|
+
if (configFile) {
|
|
2019
|
+
const bindings = yield* ctx.credentialBindings.listForSource({
|
|
2020
|
+
pluginId: MCP_PLUGIN_ID,
|
|
2021
|
+
sourceId: namespace,
|
|
2022
|
+
sourceScope: ScopeId.make(scope)
|
|
2023
|
+
});
|
|
2024
|
+
const inputForm = inputFormFromStored(
|
|
2025
|
+
bindings,
|
|
2026
|
+
updatedConfig,
|
|
2027
|
+
scope,
|
|
2028
|
+
sourceName,
|
|
2029
|
+
namespace
|
|
2030
|
+
);
|
|
2031
|
+
yield* configFile.upsertSource(toMcpConfigEntry(namespace, sourceName, inputForm)).pipe(Effect6.withSpan("mcp.plugin.config_file.upsert"));
|
|
2032
|
+
}
|
|
2033
|
+
}).pipe(
|
|
2034
|
+
Effect6.withSpan("mcp.plugin.update_source", {
|
|
2035
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
2036
|
+
})
|
|
2037
|
+
);
|
|
2038
|
+
const getSource = (namespace, scope) => ctx.storage.getSource(namespace, scope).pipe(
|
|
2039
|
+
Effect6.withSpan("mcp.plugin.get_source", {
|
|
2040
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
2041
|
+
})
|
|
2042
|
+
);
|
|
2043
|
+
return {
|
|
2044
|
+
probeEndpoint,
|
|
2045
|
+
addSource,
|
|
2046
|
+
removeSource,
|
|
2047
|
+
refreshSource,
|
|
2048
|
+
getSource,
|
|
2049
|
+
updateSource,
|
|
2050
|
+
listSourceBindings: (sourceId, sourceScope) => listMcpSourceBindings(ctx, sourceId, sourceScope),
|
|
2051
|
+
setSourceBinding: (input) => Effect6.gen(function* () {
|
|
2052
|
+
yield* validateMcpBindingTarget(ctx, {
|
|
2053
|
+
sourceId: input.sourceId,
|
|
2054
|
+
sourceScope: input.sourceScope,
|
|
2055
|
+
targetScope: input.scope
|
|
2056
|
+
});
|
|
2057
|
+
const binding = yield* ctx.credentialBindings.set({
|
|
2058
|
+
targetScope: input.scope,
|
|
2059
|
+
pluginId: MCP_PLUGIN_ID,
|
|
2060
|
+
sourceId: input.sourceId,
|
|
2061
|
+
sourceScope: input.sourceScope,
|
|
2062
|
+
slotKey: input.slot,
|
|
2063
|
+
value: input.value
|
|
2064
|
+
});
|
|
2065
|
+
return coreBindingToMcpBinding(binding);
|
|
2066
|
+
}),
|
|
2067
|
+
removeSourceBinding: (sourceId, sourceScope, slot, scope) => Effect6.gen(function* () {
|
|
2068
|
+
yield* validateMcpBindingTarget(ctx, {
|
|
2069
|
+
sourceId,
|
|
2070
|
+
sourceScope,
|
|
2071
|
+
targetScope: scope
|
|
2072
|
+
});
|
|
2073
|
+
yield* ctx.credentialBindings.remove({
|
|
2074
|
+
targetScope: ScopeId.make(scope),
|
|
2075
|
+
pluginId: MCP_PLUGIN_ID,
|
|
2076
|
+
sourceId,
|
|
2077
|
+
sourceScope: ScopeId.make(sourceScope),
|
|
2078
|
+
slotKey: slot
|
|
2079
|
+
});
|
|
2080
|
+
})
|
|
2081
|
+
};
|
|
2082
|
+
},
|
|
2083
|
+
invokeTool: ({ ctx, toolRow, args, elicit }) => Effect6.gen(function* () {
|
|
2084
|
+
const runtime = yield* ensureRuntime();
|
|
2085
|
+
const toolScope = toolRow.scope_id;
|
|
2086
|
+
const entry = yield* ctx.storage.getBinding(toolRow.id, toolScope).pipe(
|
|
2087
|
+
Effect6.withSpan("mcp.plugin.load_binding", {
|
|
2088
|
+
attributes: { "mcp.tool.name": toolRow.id }
|
|
2089
|
+
})
|
|
2090
|
+
);
|
|
2091
|
+
if (!entry) {
|
|
2092
|
+
return yield* new McpInvocationError({
|
|
2093
|
+
toolName: toolRow.id,
|
|
2094
|
+
message: `No MCP binding found for tool "${toolRow.id}"`
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
const sd = yield* ctx.storage.getSourceConfig(entry.namespace, toolScope).pipe(
|
|
2098
|
+
Effect6.withSpan("mcp.plugin.load_source_config", {
|
|
2099
|
+
attributes: { "mcp.source.namespace": entry.namespace }
|
|
2100
|
+
})
|
|
2101
|
+
);
|
|
2102
|
+
if (!sd) {
|
|
2103
|
+
return yield* new McpConnectionError({
|
|
2104
|
+
transport: "auto",
|
|
2105
|
+
message: `No MCP source config for namespace "${entry.namespace}"`
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
return yield* invokeMcpTool({
|
|
2109
|
+
toolId: toolRow.id,
|
|
2110
|
+
toolName: entry.binding.toolName,
|
|
2111
|
+
args,
|
|
2112
|
+
sourceData: sd,
|
|
2113
|
+
sourceId: entry.namespace,
|
|
2114
|
+
sourceScope: toolScope,
|
|
2115
|
+
invokerScope: ctx.scopes[0].id,
|
|
2116
|
+
resolveConnector: () => resolveConnectorInput(entry.namespace, toolScope, sd, ctx, allowStdio).pipe(
|
|
2117
|
+
Effect6.catchTags({
|
|
2118
|
+
StorageError: () => Effect6.fail(
|
|
2119
|
+
new McpConnectionError({
|
|
2120
|
+
transport: sd.transport,
|
|
2121
|
+
message: "Failed to resolve MCP connector storage state"
|
|
2122
|
+
})
|
|
2123
|
+
),
|
|
2124
|
+
UniqueViolationError: () => Effect6.fail(
|
|
2125
|
+
new McpConnectionError({
|
|
2126
|
+
transport: sd.transport,
|
|
2127
|
+
message: "Failed to resolve MCP connector storage state"
|
|
2128
|
+
})
|
|
2129
|
+
)
|
|
2130
|
+
}),
|
|
2131
|
+
Effect6.flatMap((ci) => createMcpConnector(ci)),
|
|
2132
|
+
Effect6.withSpan("mcp.plugin.resolve_connector", {
|
|
2133
|
+
attributes: {
|
|
2134
|
+
"mcp.source.namespace": entry.namespace,
|
|
2135
|
+
"mcp.source.transport": sd.transport
|
|
2136
|
+
}
|
|
2137
|
+
})
|
|
2138
|
+
),
|
|
2139
|
+
connectionCache: runtime.connectionCache,
|
|
2140
|
+
pendingConnectors: runtime.pendingConnectors,
|
|
2141
|
+
elicit
|
|
2142
|
+
});
|
|
2143
|
+
}).pipe(
|
|
2144
|
+
Effect6.withSpan("mcp.plugin.invoke_tool", {
|
|
2145
|
+
attributes: {
|
|
2146
|
+
"mcp.tool.name": toolRow.id,
|
|
2147
|
+
"mcp.tool.source_id": toolRow.source_id
|
|
2148
|
+
}
|
|
2149
|
+
})
|
|
2150
|
+
),
|
|
2151
|
+
detect: ({ ctx, url }) => Effect6.gen(function* () {
|
|
2152
|
+
const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
|
|
2153
|
+
const trimmed = url.trim();
|
|
2154
|
+
if (!trimmed) return null;
|
|
2155
|
+
const parsed = yield* Effect6.try({
|
|
2156
|
+
try: () => new URL(trimmed),
|
|
2157
|
+
catch: (cause) => cause
|
|
2158
|
+
}).pipe(Effect6.option);
|
|
2159
|
+
if (Option5.isNone(parsed)) return null;
|
|
2160
|
+
const name = parsed.value.hostname || "mcp";
|
|
2161
|
+
const namespace = deriveMcpNamespace({ endpoint: trimmed });
|
|
2162
|
+
const connector = createMcpConnector({
|
|
2163
|
+
transport: "remote",
|
|
2164
|
+
endpoint: trimmed
|
|
2165
|
+
});
|
|
2166
|
+
const connected = yield* discoverTools(connector).pipe(
|
|
2167
|
+
Effect6.map(() => true),
|
|
2168
|
+
Effect6.catch(() => Effect6.succeed(false)),
|
|
2169
|
+
Effect6.withSpan("mcp.plugin.discover_tools")
|
|
2170
|
+
);
|
|
2171
|
+
if (connected) {
|
|
2172
|
+
return SourceDetectionResult.make({
|
|
2173
|
+
kind: "mcp",
|
|
2174
|
+
confidence: "high",
|
|
2175
|
+
endpoint: trimmed,
|
|
2176
|
+
name,
|
|
2177
|
+
namespace
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
const shape = yield* probeMcpEndpointShape(trimmed, { httpClientLayer });
|
|
2181
|
+
if (shape.kind === "mcp") {
|
|
2182
|
+
return SourceDetectionResult.make({
|
|
2183
|
+
kind: "mcp",
|
|
2184
|
+
confidence: "high",
|
|
2185
|
+
endpoint: trimmed,
|
|
2186
|
+
name,
|
|
2187
|
+
namespace
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
if (urlMatchesToken(parsed.value, "mcp")) {
|
|
2191
|
+
return SourceDetectionResult.make({
|
|
2192
|
+
kind: "mcp",
|
|
2193
|
+
confidence: "low",
|
|
2194
|
+
endpoint: trimmed,
|
|
2195
|
+
name,
|
|
2196
|
+
namespace
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return null;
|
|
2200
|
+
}).pipe(
|
|
2201
|
+
Effect6.catch(() => Effect6.succeed(null)),
|
|
2202
|
+
Effect6.withSpan("mcp.plugin.detect", {
|
|
2203
|
+
attributes: { "mcp.endpoint": url }
|
|
2204
|
+
})
|
|
2205
|
+
),
|
|
2206
|
+
// Honor upstream destructiveHint from MCP ToolAnnotations.
|
|
2207
|
+
// Bindings are fetched per scope so shadowed sources (e.g. an org-level
|
|
2208
|
+
// source overridden per-user) each resolve against their own scope's
|
|
2209
|
+
// row rather than collapsing onto whichever row the scoped adapter
|
|
2210
|
+
// sees first.
|
|
2211
|
+
resolveAnnotations: ({ ctx, sourceId, toolRows }) => Effect6.gen(function* () {
|
|
2212
|
+
const scopes = new Set(toolRows.map((row) => row.scope_id));
|
|
2213
|
+
const entries = yield* Effect6.forEach(
|
|
2214
|
+
[...scopes],
|
|
2215
|
+
(scope) => Effect6.gen(function* () {
|
|
2216
|
+
const list = yield* ctx.storage.listBindingsBySource(sourceId, scope);
|
|
2217
|
+
const byId = new Map(list.map((e) => [e.toolId, e.binding]));
|
|
2218
|
+
return [scope, byId];
|
|
2219
|
+
}),
|
|
2220
|
+
{ concurrency: "unbounded" }
|
|
2221
|
+
);
|
|
2222
|
+
const byScope = new Map(entries);
|
|
2223
|
+
const out = {};
|
|
2224
|
+
for (const row of toolRows) {
|
|
2225
|
+
const binding = byScope.get(row.scope_id)?.get(row.id);
|
|
2226
|
+
const ann = binding?.annotations;
|
|
2227
|
+
if (ann?.destructiveHint === true) {
|
|
2228
|
+
out[row.id] = {
|
|
2229
|
+
requiresApproval: true,
|
|
2230
|
+
approvalDescription: ann.title ?? binding?.toolName ?? row.id
|
|
2231
|
+
};
|
|
2232
|
+
} else {
|
|
2233
|
+
out[row.id] = { requiresApproval: false };
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return out;
|
|
2237
|
+
}),
|
|
2238
|
+
removeSource: ({ ctx, sourceId, scope }) => Effect6.gen(function* () {
|
|
2239
|
+
yield* ctx.transaction(
|
|
2240
|
+
Effect6.gen(function* () {
|
|
2241
|
+
yield* ctx.credentialBindings.removeForSource({
|
|
2242
|
+
pluginId: MCP_PLUGIN_ID,
|
|
2243
|
+
sourceId,
|
|
2244
|
+
sourceScope: ScopeId.make(scope)
|
|
2245
|
+
});
|
|
2246
|
+
yield* ctx.storage.removeBindingsByNamespace(sourceId, scope);
|
|
2247
|
+
yield* ctx.storage.removeSource(sourceId, scope);
|
|
2248
|
+
})
|
|
2249
|
+
);
|
|
2250
|
+
if (options?.configFile) {
|
|
2251
|
+
yield* options.configFile.removeSource(sourceId);
|
|
2252
|
+
}
|
|
2253
|
+
}),
|
|
2254
|
+
usagesForSecret: () => Effect6.succeed([]),
|
|
2255
|
+
usagesForConnection: () => Effect6.succeed([]),
|
|
2256
|
+
refreshSource: () => Effect6.void,
|
|
2257
|
+
// Connection refresh for oauth2-minted sources is owned by the
|
|
2258
|
+
// canonical `"oauth2"` ConnectionProvider that core registers via
|
|
2259
|
+
// `makeOAuth2Service`. No MCP-specific provider needed.
|
|
2260
|
+
close: () => Effect6.gen(function* () {
|
|
2261
|
+
const runtime = runtimeRef.current;
|
|
2262
|
+
if (runtime) {
|
|
2263
|
+
runtime.pendingConnectors.clear();
|
|
2264
|
+
yield* ScopedCache2.invalidateAll(runtime.connectionCache);
|
|
2265
|
+
yield* Scope.close(runtime.cacheScope, Exit2.void);
|
|
2266
|
+
runtimeRef.current = null;
|
|
2267
|
+
}
|
|
2268
|
+
}).pipe(Effect6.withSpan("mcp.plugin.close"))
|
|
2269
|
+
};
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
export {
|
|
2273
|
+
mcpSchema,
|
|
2274
|
+
makeMcpStore,
|
|
2275
|
+
mcpPlugin
|
|
2276
|
+
};
|
|
2277
|
+
//# sourceMappingURL=chunk-NQT7NAGE.js.map
|