@executor-js/plugin-mcp 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -15
- package/dist/api/group.d.ts +113 -144
- package/dist/api/handlers.d.ts +2 -2
- package/dist/chunk-C2GNZGFJ.js +1622 -0
- package/dist/chunk-C2GNZGFJ.js.map +1 -0
- package/dist/core.js +8 -52
- package/dist/core.js.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/promise.d.ts +2 -6
- package/dist/react/AddMcpSource.d.ts +2 -0
- package/dist/react/McpSignInButton.d.ts +3 -0
- package/dist/react/atoms.d.ts +153 -0
- package/dist/react/client.d.ts +104 -3
- package/dist/react/index.d.ts +3 -2
- package/dist/react/plugin-client.d.ts +2 -0
- package/dist/react/source-plugin.d.ts +13 -1
- package/dist/sdk/binding-store.d.ts +83 -25
- package/dist/sdk/connection-pool.test.d.ts +1 -0
- package/dist/sdk/connection.d.ts +3 -1
- package/dist/sdk/cross-user-isolation.test.d.ts +1 -0
- package/dist/sdk/errors.d.ts +15 -23
- package/dist/sdk/index.d.ts +3 -3
- package/dist/sdk/invoke.d.ts +18 -17
- package/dist/sdk/manifest.d.ts +3 -0
- package/dist/sdk/per-user-auth-isolation.test.d.ts +1 -0
- package/dist/sdk/plugin.d.ts +238 -43
- package/dist/sdk/probe-shape.d.ts +39 -0
- package/dist/sdk/probe-shape.test.d.ts +1 -0
- package/dist/sdk/stdio-connector.d.ts +8 -0
- package/dist/sdk/stored-source.d.ts +31 -105
- package/dist/sdk/test-utils.d.ts +16 -0
- package/dist/sdk/types.d.ts +92 -93
- package/dist/stdio-connector-KNHLETKM.js +12 -0
- package/dist/stdio-connector-KNHLETKM.js.map +1 -0
- package/package.json +11 -21
- package/dist/chunk-X3JTTDWJ.js +0 -1255
- package/dist/chunk-X3JTTDWJ.js.map +0 -1
- package/dist/react/McpSourceSummary.d.ts +0 -3
- package/dist/sdk/config-file-store.d.ts +0 -10
- package/dist/sdk/oauth.d.ts +0 -40
|
@@ -0,0 +1,1622 @@
|
|
|
1
|
+
// src/sdk/types.ts
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { SecretBackedMap, SecretBackedValue } from "@executor-js/sdk/core";
|
|
4
|
+
var McpRemoteTransport = Schema.Literals(["streamable-http", "sse", "auto"]);
|
|
5
|
+
var McpTransport = Schema.Literals(["streamable-http", "sse", "stdio", "auto"]);
|
|
6
|
+
var JsonObject = Schema.Record(Schema.String, Schema.Unknown);
|
|
7
|
+
var McpConnectionAuth = Schema.Union([
|
|
8
|
+
Schema.Struct({ kind: Schema.Literal("none") }),
|
|
9
|
+
Schema.Struct({
|
|
10
|
+
kind: Schema.Literal("header"),
|
|
11
|
+
headerName: Schema.String,
|
|
12
|
+
secretId: Schema.String,
|
|
13
|
+
prefix: Schema.optional(Schema.String)
|
|
14
|
+
}),
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
kind: Schema.Literal("oauth2"),
|
|
17
|
+
connectionId: Schema.String,
|
|
18
|
+
clientIdSecretId: Schema.optional(Schema.String),
|
|
19
|
+
clientSecretSecretId: Schema.optional(Schema.NullOr(Schema.String))
|
|
20
|
+
})
|
|
21
|
+
]);
|
|
22
|
+
var StringMap = Schema.Record(Schema.String, Schema.String);
|
|
23
|
+
var McpRemoteSourceData = Schema.Struct({
|
|
24
|
+
transport: Schema.Literal("remote"),
|
|
25
|
+
/** The MCP server endpoint URL */
|
|
26
|
+
endpoint: Schema.String,
|
|
27
|
+
/** Transport preference for this remote source */
|
|
28
|
+
remoteTransport: McpRemoteTransport.pipe(
|
|
29
|
+
Schema.optionalKey,
|
|
30
|
+
Schema.withConstructorDefault(Effect.succeed("auto"))
|
|
31
|
+
),
|
|
32
|
+
/** Extra query params appended to the endpoint URL */
|
|
33
|
+
queryParams: Schema.optional(SecretBackedMap),
|
|
34
|
+
/** Extra headers sent on every request */
|
|
35
|
+
headers: Schema.optional(SecretBackedMap),
|
|
36
|
+
/** Auth configuration */
|
|
37
|
+
auth: McpConnectionAuth
|
|
38
|
+
});
|
|
39
|
+
var McpStdioSourceData = Schema.Struct({
|
|
40
|
+
transport: Schema.Literal("stdio"),
|
|
41
|
+
/** The command to run */
|
|
42
|
+
command: Schema.String,
|
|
43
|
+
/** Arguments to the command */
|
|
44
|
+
args: Schema.optional(Schema.Array(Schema.String)),
|
|
45
|
+
/** Environment variables */
|
|
46
|
+
env: Schema.optional(StringMap),
|
|
47
|
+
/** Working directory */
|
|
48
|
+
cwd: Schema.optional(Schema.String)
|
|
49
|
+
});
|
|
50
|
+
var McpStoredSourceData = Schema.Union([McpRemoteSourceData, McpStdioSourceData]);
|
|
51
|
+
var McpToolAnnotations = Schema.Struct({
|
|
52
|
+
title: Schema.optional(Schema.String),
|
|
53
|
+
readOnlyHint: Schema.optional(Schema.Boolean),
|
|
54
|
+
destructiveHint: Schema.optional(Schema.Boolean),
|
|
55
|
+
idempotentHint: Schema.optional(Schema.Boolean),
|
|
56
|
+
openWorldHint: Schema.optional(Schema.Boolean)
|
|
57
|
+
});
|
|
58
|
+
var McpToolBinding = class extends Schema.Class("McpToolBinding")({
|
|
59
|
+
toolId: Schema.String,
|
|
60
|
+
toolName: Schema.String,
|
|
61
|
+
description: Schema.NullOr(Schema.String),
|
|
62
|
+
inputSchema: Schema.optional(Schema.Unknown),
|
|
63
|
+
outputSchema: Schema.optional(Schema.Unknown),
|
|
64
|
+
annotations: Schema.optional(McpToolAnnotations)
|
|
65
|
+
}) {
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/sdk/binding-store.ts
|
|
69
|
+
import { Effect as Effect2, Schema as Schema2 } from "effect";
|
|
70
|
+
import {
|
|
71
|
+
defineSchema
|
|
72
|
+
} from "@executor-js/sdk/core";
|
|
73
|
+
var mcpSchema = defineSchema({
|
|
74
|
+
mcp_source: {
|
|
75
|
+
fields: {
|
|
76
|
+
id: { type: "string", required: true },
|
|
77
|
+
scope_id: { type: "string", required: true, index: true },
|
|
78
|
+
name: { type: "string", required: true },
|
|
79
|
+
config: { type: "json", required: true },
|
|
80
|
+
created_at: { type: "date", required: true }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
mcp_binding: {
|
|
84
|
+
fields: {
|
|
85
|
+
id: { type: "string", required: true },
|
|
86
|
+
scope_id: { type: "string", required: true, index: true },
|
|
87
|
+
source_id: { type: "string", required: true, index: true },
|
|
88
|
+
binding: { type: "json", required: true },
|
|
89
|
+
created_at: { type: "date", required: true }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
var decodeSourceData = Schema2.decodeUnknownSync(McpStoredSourceData);
|
|
94
|
+
var encodeSourceData = Schema2.encodeSync(McpStoredSourceData);
|
|
95
|
+
var decodeBinding = Schema2.decodeUnknownSync(McpToolBinding);
|
|
96
|
+
var encodeBinding = Schema2.encodeSync(McpToolBinding);
|
|
97
|
+
var coerceJson = (value) => {
|
|
98
|
+
if (typeof value !== "string") return value;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(value);
|
|
101
|
+
} catch {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var makeMcpStore = ({
|
|
106
|
+
adapter: db
|
|
107
|
+
}) => {
|
|
108
|
+
return {
|
|
109
|
+
listBindingsBySource: (namespace, scope) => Effect2.gen(function* () {
|
|
110
|
+
const rows = yield* db.findMany({
|
|
111
|
+
model: "mcp_binding",
|
|
112
|
+
where: [
|
|
113
|
+
{ field: "source_id", value: namespace },
|
|
114
|
+
{ field: "scope_id", value: scope }
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
return rows.map((row) => ({
|
|
118
|
+
toolId: row.id,
|
|
119
|
+
binding: decodeBinding(coerceJson(row.binding))
|
|
120
|
+
}));
|
|
121
|
+
}),
|
|
122
|
+
getBinding: (toolId, scope) => Effect2.gen(function* () {
|
|
123
|
+
const row = yield* db.findOne({
|
|
124
|
+
model: "mcp_binding",
|
|
125
|
+
where: [
|
|
126
|
+
{ field: "id", value: toolId },
|
|
127
|
+
{ field: "scope_id", value: scope }
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
if (!row) return null;
|
|
131
|
+
const binding = decodeBinding(coerceJson(row.binding));
|
|
132
|
+
return { binding, namespace: row.source_id };
|
|
133
|
+
}),
|
|
134
|
+
putBindings: (namespace, scope, entries) => Effect2.gen(function* () {
|
|
135
|
+
if (entries.length === 0) return;
|
|
136
|
+
const now = /* @__PURE__ */ new Date();
|
|
137
|
+
yield* db.createMany({
|
|
138
|
+
model: "mcp_binding",
|
|
139
|
+
data: entries.map((e) => ({
|
|
140
|
+
id: e.toolId,
|
|
141
|
+
scope_id: scope,
|
|
142
|
+
source_id: namespace,
|
|
143
|
+
binding: encodeBinding(e.binding),
|
|
144
|
+
created_at: now
|
|
145
|
+
})),
|
|
146
|
+
forceAllowId: true
|
|
147
|
+
});
|
|
148
|
+
}),
|
|
149
|
+
removeBindingsByNamespace: (namespace, scope) => db.deleteMany({
|
|
150
|
+
model: "mcp_binding",
|
|
151
|
+
where: [
|
|
152
|
+
{ field: "source_id", value: namespace },
|
|
153
|
+
{ field: "scope_id", value: scope }
|
|
154
|
+
]
|
|
155
|
+
}).pipe(Effect2.asVoid),
|
|
156
|
+
getSource: (namespace, scope) => Effect2.gen(function* () {
|
|
157
|
+
const row = yield* db.findOne({
|
|
158
|
+
model: "mcp_source",
|
|
159
|
+
where: [
|
|
160
|
+
{ field: "id", value: namespace },
|
|
161
|
+
{ field: "scope_id", value: scope }
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
if (!row) return null;
|
|
165
|
+
return {
|
|
166
|
+
namespace: row.id,
|
|
167
|
+
scope: row.scope_id,
|
|
168
|
+
name: row.name,
|
|
169
|
+
config: decodeSourceData(coerceJson(row.config))
|
|
170
|
+
};
|
|
171
|
+
}),
|
|
172
|
+
getSourceConfig: (namespace, scope) => Effect2.gen(function* () {
|
|
173
|
+
const row = yield* db.findOne({
|
|
174
|
+
model: "mcp_source",
|
|
175
|
+
where: [
|
|
176
|
+
{ field: "id", value: namespace },
|
|
177
|
+
{ field: "scope_id", value: scope }
|
|
178
|
+
]
|
|
179
|
+
});
|
|
180
|
+
if (!row) return null;
|
|
181
|
+
return decodeSourceData(coerceJson(row.config));
|
|
182
|
+
}),
|
|
183
|
+
putSource: (source) => Effect2.gen(function* () {
|
|
184
|
+
const now = /* @__PURE__ */ new Date();
|
|
185
|
+
yield* db.delete({
|
|
186
|
+
model: "mcp_source",
|
|
187
|
+
where: [
|
|
188
|
+
{ field: "id", value: source.namespace },
|
|
189
|
+
{ field: "scope_id", value: source.scope }
|
|
190
|
+
]
|
|
191
|
+
});
|
|
192
|
+
yield* db.create({
|
|
193
|
+
model: "mcp_source",
|
|
194
|
+
data: {
|
|
195
|
+
id: source.namespace,
|
|
196
|
+
scope_id: source.scope,
|
|
197
|
+
name: source.name,
|
|
198
|
+
config: encodeSourceData(source.config),
|
|
199
|
+
created_at: now
|
|
200
|
+
},
|
|
201
|
+
forceAllowId: true
|
|
202
|
+
});
|
|
203
|
+
}),
|
|
204
|
+
removeSource: (namespace, scope) => Effect2.gen(function* () {
|
|
205
|
+
yield* db.deleteMany({
|
|
206
|
+
model: "mcp_binding",
|
|
207
|
+
where: [
|
|
208
|
+
{ field: "source_id", value: namespace },
|
|
209
|
+
{ field: "scope_id", value: scope }
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
yield* db.delete({
|
|
213
|
+
model: "mcp_source",
|
|
214
|
+
where: [
|
|
215
|
+
{ field: "id", value: namespace },
|
|
216
|
+
{ field: "scope_id", value: scope }
|
|
217
|
+
]
|
|
218
|
+
});
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/sdk/plugin.ts
|
|
224
|
+
import { Duration, Effect as Effect8, Exit as Exit2, Result, Scope, ScopedCache as ScopedCache2 } from "effect";
|
|
225
|
+
|
|
226
|
+
// src/api/group.ts
|
|
227
|
+
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
|
|
228
|
+
import { Schema as Schema5 } from "effect";
|
|
229
|
+
import { ScopeId, SecretBackedMap as SecretBackedMap2 } from "@executor-js/sdk/core";
|
|
230
|
+
import { InternalError } from "@executor-js/api";
|
|
231
|
+
|
|
232
|
+
// src/sdk/errors.ts
|
|
233
|
+
import { Schema as Schema3 } from "effect";
|
|
234
|
+
var McpConnectionError = class extends Schema3.TaggedErrorClass()(
|
|
235
|
+
"McpConnectionError",
|
|
236
|
+
{
|
|
237
|
+
transport: Schema3.String,
|
|
238
|
+
message: Schema3.String
|
|
239
|
+
},
|
|
240
|
+
{ httpApiStatus: 400 }
|
|
241
|
+
) {
|
|
242
|
+
};
|
|
243
|
+
var McpToolDiscoveryError = class extends Schema3.TaggedErrorClass()(
|
|
244
|
+
"McpToolDiscoveryError",
|
|
245
|
+
{
|
|
246
|
+
stage: Schema3.Literals(["connect", "list_tools"]),
|
|
247
|
+
message: Schema3.String
|
|
248
|
+
},
|
|
249
|
+
{ httpApiStatus: 400 }
|
|
250
|
+
) {
|
|
251
|
+
};
|
|
252
|
+
var McpInvocationError = class extends Schema3.TaggedErrorClass()(
|
|
253
|
+
"McpInvocationError",
|
|
254
|
+
{
|
|
255
|
+
toolName: Schema3.String,
|
|
256
|
+
message: Schema3.String
|
|
257
|
+
},
|
|
258
|
+
{ httpApiStatus: 400 }
|
|
259
|
+
) {
|
|
260
|
+
};
|
|
261
|
+
var McpOAuthError = class extends Schema3.TaggedErrorClass()(
|
|
262
|
+
"McpOAuthError",
|
|
263
|
+
{
|
|
264
|
+
message: Schema3.String
|
|
265
|
+
},
|
|
266
|
+
{ httpApiStatus: 400 }
|
|
267
|
+
) {
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/sdk/stored-source.ts
|
|
271
|
+
import { Schema as Schema4 } from "effect";
|
|
272
|
+
var McpStoredSourceSchema = class extends Schema4.Class("McpStoredSource")({
|
|
273
|
+
namespace: Schema4.String,
|
|
274
|
+
name: Schema4.String,
|
|
275
|
+
config: McpStoredSourceData
|
|
276
|
+
}) {
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// src/api/group.ts
|
|
280
|
+
var ScopeParams = { scopeId: ScopeId };
|
|
281
|
+
var SourceParams = { scopeId: ScopeId, namespace: Schema5.String };
|
|
282
|
+
var AuthPayload = Schema5.Union([
|
|
283
|
+
Schema5.Struct({ kind: Schema5.Literal("none") }),
|
|
284
|
+
Schema5.Struct({
|
|
285
|
+
kind: Schema5.Literal("header"),
|
|
286
|
+
headerName: Schema5.String,
|
|
287
|
+
secretId: Schema5.String,
|
|
288
|
+
prefix: Schema5.optional(Schema5.String)
|
|
289
|
+
}),
|
|
290
|
+
Schema5.Struct({
|
|
291
|
+
kind: Schema5.Literal("oauth2"),
|
|
292
|
+
/** Stable id of the SDK Connection minted by `completeOAuth`. The
|
|
293
|
+
* backing access/refresh secrets live on the connection row; the
|
|
294
|
+
* source only needs this pointer. */
|
|
295
|
+
connectionId: Schema5.String,
|
|
296
|
+
clientIdSecretId: Schema5.optional(Schema5.String),
|
|
297
|
+
clientSecretSecretId: Schema5.optional(Schema5.NullOr(Schema5.String))
|
|
298
|
+
})
|
|
299
|
+
]);
|
|
300
|
+
var StringMap2 = Schema5.Record(Schema5.String, Schema5.String);
|
|
301
|
+
var AddRemoteSourcePayload = Schema5.Struct({
|
|
302
|
+
transport: Schema5.Literal("remote"),
|
|
303
|
+
name: Schema5.String,
|
|
304
|
+
endpoint: Schema5.String,
|
|
305
|
+
remoteTransport: Schema5.optional(Schema5.Literals(["streamable-http", "sse", "auto"])),
|
|
306
|
+
namespace: Schema5.optional(Schema5.String),
|
|
307
|
+
queryParams: Schema5.optional(SecretBackedMap2),
|
|
308
|
+
headers: Schema5.optional(SecretBackedMap2),
|
|
309
|
+
auth: Schema5.optional(AuthPayload)
|
|
310
|
+
});
|
|
311
|
+
var AddStdioSourcePayload = Schema5.Struct({
|
|
312
|
+
transport: Schema5.Literal("stdio"),
|
|
313
|
+
name: Schema5.String,
|
|
314
|
+
command: Schema5.String,
|
|
315
|
+
args: Schema5.optional(Schema5.Array(Schema5.String)),
|
|
316
|
+
env: Schema5.optional(StringMap2),
|
|
317
|
+
cwd: Schema5.optional(Schema5.String),
|
|
318
|
+
namespace: Schema5.optional(Schema5.String)
|
|
319
|
+
});
|
|
320
|
+
var AddSourcePayload = Schema5.Union([AddRemoteSourcePayload, AddStdioSourcePayload]);
|
|
321
|
+
var UpdateSourcePayload = Schema5.Struct({
|
|
322
|
+
name: Schema5.optional(Schema5.String),
|
|
323
|
+
endpoint: Schema5.optional(Schema5.String),
|
|
324
|
+
headers: Schema5.optional(SecretBackedMap2),
|
|
325
|
+
queryParams: Schema5.optional(SecretBackedMap2),
|
|
326
|
+
auth: Schema5.optional(AuthPayload)
|
|
327
|
+
});
|
|
328
|
+
var UpdateSourceResponse = Schema5.Struct({
|
|
329
|
+
updated: Schema5.Boolean
|
|
330
|
+
});
|
|
331
|
+
var ProbeEndpointPayload = Schema5.Struct({
|
|
332
|
+
endpoint: Schema5.String,
|
|
333
|
+
headers: Schema5.optional(SecretBackedMap2),
|
|
334
|
+
queryParams: Schema5.optional(SecretBackedMap2)
|
|
335
|
+
});
|
|
336
|
+
var ProbeEndpointResponse = Schema5.Struct({
|
|
337
|
+
connected: Schema5.Boolean,
|
|
338
|
+
requiresOAuth: Schema5.Boolean,
|
|
339
|
+
name: Schema5.String,
|
|
340
|
+
namespace: Schema5.String,
|
|
341
|
+
toolCount: Schema5.NullOr(Schema5.Number),
|
|
342
|
+
serverName: Schema5.NullOr(Schema5.String)
|
|
343
|
+
});
|
|
344
|
+
var NamespacePayload = Schema5.Struct({
|
|
345
|
+
namespace: Schema5.String
|
|
346
|
+
});
|
|
347
|
+
var AddSourceResponse = Schema5.Struct({
|
|
348
|
+
toolCount: Schema5.Number,
|
|
349
|
+
namespace: Schema5.String
|
|
350
|
+
});
|
|
351
|
+
var RefreshSourceResponse = Schema5.Struct({
|
|
352
|
+
toolCount: Schema5.Number
|
|
353
|
+
});
|
|
354
|
+
var RemoveSourceResponse = Schema5.Struct({
|
|
355
|
+
removed: Schema5.Boolean
|
|
356
|
+
});
|
|
357
|
+
var McpGroup = HttpApiGroup.make("mcp").add(
|
|
358
|
+
HttpApiEndpoint.post("probeEndpoint", "/scopes/:scopeId/mcp/probe", {
|
|
359
|
+
params: ScopeParams,
|
|
360
|
+
payload: ProbeEndpointPayload,
|
|
361
|
+
success: ProbeEndpointResponse,
|
|
362
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
363
|
+
})
|
|
364
|
+
).add(
|
|
365
|
+
HttpApiEndpoint.post("addSource", "/scopes/:scopeId/mcp/sources", {
|
|
366
|
+
params: ScopeParams,
|
|
367
|
+
payload: AddSourcePayload,
|
|
368
|
+
success: AddSourceResponse,
|
|
369
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
370
|
+
})
|
|
371
|
+
).add(
|
|
372
|
+
HttpApiEndpoint.post("removeSource", "/scopes/:scopeId/mcp/sources/remove", {
|
|
373
|
+
params: ScopeParams,
|
|
374
|
+
payload: NamespacePayload,
|
|
375
|
+
success: RemoveSourceResponse,
|
|
376
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
377
|
+
})
|
|
378
|
+
).add(
|
|
379
|
+
HttpApiEndpoint.post("refreshSource", "/scopes/:scopeId/mcp/sources/refresh", {
|
|
380
|
+
params: ScopeParams,
|
|
381
|
+
payload: NamespacePayload,
|
|
382
|
+
success: RefreshSourceResponse,
|
|
383
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
384
|
+
})
|
|
385
|
+
).add(
|
|
386
|
+
HttpApiEndpoint.get("getSource", "/scopes/:scopeId/mcp/sources/:namespace", {
|
|
387
|
+
params: SourceParams,
|
|
388
|
+
success: Schema5.NullOr(McpStoredSourceSchema),
|
|
389
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
390
|
+
})
|
|
391
|
+
).add(
|
|
392
|
+
HttpApiEndpoint.patch("updateSource", "/scopes/:scopeId/mcp/sources/:namespace", {
|
|
393
|
+
params: SourceParams,
|
|
394
|
+
payload: UpdateSourcePayload,
|
|
395
|
+
success: UpdateSourceResponse,
|
|
396
|
+
error: [InternalError, McpConnectionError, McpToolDiscoveryError]
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// src/api/handlers.ts
|
|
401
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
402
|
+
import { Context, Effect as Effect3 } from "effect";
|
|
403
|
+
import { addGroup, capture } from "@executor-js/api";
|
|
404
|
+
var McpExtensionService = class extends Context.Service()("McpExtensionService") {
|
|
405
|
+
};
|
|
406
|
+
var ExecutorApiWithMcp = addGroup(McpGroup);
|
|
407
|
+
var toSourceConfig = (payload, scope) => {
|
|
408
|
+
if (payload.transport === "stdio") {
|
|
409
|
+
const p2 = payload;
|
|
410
|
+
return {
|
|
411
|
+
transport: "stdio",
|
|
412
|
+
scope,
|
|
413
|
+
name: p2.name,
|
|
414
|
+
command: p2.command,
|
|
415
|
+
args: p2.args ? [...p2.args] : void 0,
|
|
416
|
+
env: p2.env,
|
|
417
|
+
cwd: p2.cwd,
|
|
418
|
+
namespace: p2.namespace
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const p = payload;
|
|
422
|
+
return {
|
|
423
|
+
transport: "remote",
|
|
424
|
+
scope,
|
|
425
|
+
name: p.name,
|
|
426
|
+
endpoint: p.endpoint,
|
|
427
|
+
remoteTransport: p.remoteTransport,
|
|
428
|
+
queryParams: p.queryParams,
|
|
429
|
+
headers: p.headers,
|
|
430
|
+
namespace: p.namespace,
|
|
431
|
+
auth: p.auth
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
var McpHandlers = HttpApiBuilder.group(
|
|
435
|
+
ExecutorApiWithMcp,
|
|
436
|
+
"mcp",
|
|
437
|
+
(handlers) => handlers.handle(
|
|
438
|
+
"probeEndpoint",
|
|
439
|
+
({ payload }) => capture(
|
|
440
|
+
Effect3.gen(function* () {
|
|
441
|
+
const ext = yield* McpExtensionService;
|
|
442
|
+
return yield* ext.probeEndpoint(payload);
|
|
443
|
+
})
|
|
444
|
+
)
|
|
445
|
+
).handle(
|
|
446
|
+
"addSource",
|
|
447
|
+
({ params: path, payload }) => capture(
|
|
448
|
+
Effect3.gen(function* () {
|
|
449
|
+
const ext = yield* McpExtensionService;
|
|
450
|
+
return yield* ext.addSource(
|
|
451
|
+
toSourceConfig(payload, path.scopeId)
|
|
452
|
+
);
|
|
453
|
+
})
|
|
454
|
+
)
|
|
455
|
+
).handle(
|
|
456
|
+
"removeSource",
|
|
457
|
+
({ params: path, payload }) => capture(
|
|
458
|
+
Effect3.gen(function* () {
|
|
459
|
+
const ext = yield* McpExtensionService;
|
|
460
|
+
yield* ext.removeSource(payload.namespace, path.scopeId);
|
|
461
|
+
return { removed: true };
|
|
462
|
+
})
|
|
463
|
+
)
|
|
464
|
+
).handle(
|
|
465
|
+
"refreshSource",
|
|
466
|
+
({ params: path, payload }) => capture(
|
|
467
|
+
Effect3.gen(function* () {
|
|
468
|
+
const ext = yield* McpExtensionService;
|
|
469
|
+
return yield* ext.refreshSource(payload.namespace, path.scopeId);
|
|
470
|
+
})
|
|
471
|
+
)
|
|
472
|
+
).handle(
|
|
473
|
+
"getSource",
|
|
474
|
+
({ params: path }) => capture(
|
|
475
|
+
Effect3.gen(function* () {
|
|
476
|
+
const ext = yield* McpExtensionService;
|
|
477
|
+
const source = yield* ext.getSource(path.namespace, path.scopeId);
|
|
478
|
+
return source ? new McpStoredSourceSchema({
|
|
479
|
+
namespace: source.namespace,
|
|
480
|
+
name: source.name,
|
|
481
|
+
config: source.config
|
|
482
|
+
}) : null;
|
|
483
|
+
})
|
|
484
|
+
)
|
|
485
|
+
).handle(
|
|
486
|
+
"updateSource",
|
|
487
|
+
({ params: path, payload }) => capture(
|
|
488
|
+
Effect3.gen(function* () {
|
|
489
|
+
const ext = yield* McpExtensionService;
|
|
490
|
+
yield* ext.updateSource(path.namespace, path.scopeId, {
|
|
491
|
+
name: payload.name,
|
|
492
|
+
endpoint: payload.endpoint,
|
|
493
|
+
headers: payload.headers,
|
|
494
|
+
queryParams: payload.queryParams,
|
|
495
|
+
auth: payload.auth
|
|
496
|
+
});
|
|
497
|
+
return { updated: true };
|
|
498
|
+
})
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// src/sdk/plugin.ts
|
|
504
|
+
import {
|
|
505
|
+
SourceDetectionResult,
|
|
506
|
+
definePlugin,
|
|
507
|
+
resolveSecretBackedMap as resolveSharedSecretBackedMap
|
|
508
|
+
} from "@executor-js/sdk/core";
|
|
509
|
+
|
|
510
|
+
// src/sdk/connection.ts
|
|
511
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
512
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
513
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
514
|
+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
|
|
515
|
+
import { Effect as Effect4 } from "effect";
|
|
516
|
+
var buildEndpointUrl = (endpoint, queryParams) => {
|
|
517
|
+
const url = new URL(endpoint);
|
|
518
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
519
|
+
url.searchParams.set(key, value);
|
|
520
|
+
}
|
|
521
|
+
return url;
|
|
522
|
+
};
|
|
523
|
+
var createClient = () => new Client(
|
|
524
|
+
{ name: "executor-mcp", version: "0.1.0" },
|
|
525
|
+
{
|
|
526
|
+
capabilities: { elicitation: { form: {}, url: {} } },
|
|
527
|
+
jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
var connectionFromClient = (client) => ({
|
|
531
|
+
client,
|
|
532
|
+
close: () => client.close()
|
|
533
|
+
});
|
|
534
|
+
var connectClient = (input) => Effect4.gen(function* () {
|
|
535
|
+
const client = createClient();
|
|
536
|
+
const transportInstance = input.createTransport();
|
|
537
|
+
yield* Effect4.tryPromise({
|
|
538
|
+
try: () => client.connect(transportInstance),
|
|
539
|
+
catch: (cause) => new McpConnectionError({
|
|
540
|
+
transport: input.transport,
|
|
541
|
+
message: `Failed connecting via ${input.transport}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
542
|
+
})
|
|
543
|
+
}).pipe(
|
|
544
|
+
Effect4.withSpan("plugin.mcp.connection.handshake", {
|
|
545
|
+
attributes: { "plugin.mcp.transport": input.transport }
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
return connectionFromClient(client);
|
|
549
|
+
});
|
|
550
|
+
var createMcpConnector = (input) => {
|
|
551
|
+
if (input.transport === "stdio") {
|
|
552
|
+
const command = input.command.trim();
|
|
553
|
+
if (!command) {
|
|
554
|
+
return Effect4.fail(
|
|
555
|
+
new McpConnectionError({
|
|
556
|
+
transport: "stdio",
|
|
557
|
+
message: "MCP stdio transport requires a command"
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return Effect4.gen(function* () {
|
|
562
|
+
const { createStdioTransport } = yield* Effect4.tryPromise({
|
|
563
|
+
try: () => import("./stdio-connector-KNHLETKM.js"),
|
|
564
|
+
catch: (cause) => new McpConnectionError({
|
|
565
|
+
transport: "stdio",
|
|
566
|
+
message: `Failed to load stdio transport module: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
567
|
+
})
|
|
568
|
+
});
|
|
569
|
+
return yield* connectClient({
|
|
570
|
+
transport: "stdio",
|
|
571
|
+
createTransport: () => createStdioTransport({
|
|
572
|
+
command,
|
|
573
|
+
args: input.args,
|
|
574
|
+
env: input.env,
|
|
575
|
+
cwd: input.cwd?.trim().length ? input.cwd.trim() : void 0
|
|
576
|
+
})
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const headers = input.headers ?? {};
|
|
581
|
+
const remoteTransport = input.remoteTransport ?? "auto";
|
|
582
|
+
const requestInit = Object.keys(headers).length > 0 ? { headers } : void 0;
|
|
583
|
+
const endpoint = buildEndpointUrl(input.endpoint, input.queryParams ?? {});
|
|
584
|
+
const connectStreamableHttp = connectClient({
|
|
585
|
+
transport: "streamable-http",
|
|
586
|
+
createTransport: () => new StreamableHTTPClientTransport(endpoint, {
|
|
587
|
+
requestInit,
|
|
588
|
+
authProvider: input.authProvider
|
|
589
|
+
})
|
|
590
|
+
});
|
|
591
|
+
const connectSse = connectClient({
|
|
592
|
+
transport: "sse",
|
|
593
|
+
createTransport: () => new SSEClientTransport(endpoint, {
|
|
594
|
+
requestInit,
|
|
595
|
+
authProvider: input.authProvider
|
|
596
|
+
})
|
|
597
|
+
});
|
|
598
|
+
if (remoteTransport === "streamable-http") return connectStreamableHttp;
|
|
599
|
+
if (remoteTransport === "sse") return connectSse;
|
|
600
|
+
return connectStreamableHttp.pipe(Effect4.catch(() => connectSse));
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/sdk/discover.ts
|
|
604
|
+
import { Effect as Effect5 } from "effect";
|
|
605
|
+
|
|
606
|
+
// src/sdk/manifest.ts
|
|
607
|
+
import { Schema as Schema6 } from "effect";
|
|
608
|
+
var ListedTool = Schema6.Struct({
|
|
609
|
+
name: Schema6.String,
|
|
610
|
+
description: Schema6.optional(Schema6.NullOr(Schema6.String)),
|
|
611
|
+
inputSchema: Schema6.optional(Schema6.Unknown),
|
|
612
|
+
parameters: Schema6.optional(Schema6.Unknown),
|
|
613
|
+
outputSchema: Schema6.optional(Schema6.Unknown),
|
|
614
|
+
annotations: Schema6.optional(McpToolAnnotations)
|
|
615
|
+
});
|
|
616
|
+
var ListToolsResult = Schema6.Struct({
|
|
617
|
+
tools: Schema6.Array(ListedTool)
|
|
618
|
+
});
|
|
619
|
+
var ServerInfo = Schema6.Struct({
|
|
620
|
+
name: Schema6.optional(Schema6.String),
|
|
621
|
+
version: Schema6.optional(Schema6.String)
|
|
622
|
+
});
|
|
623
|
+
var decodeListToolsResult = Schema6.decodeUnknownOption(ListToolsResult);
|
|
624
|
+
var decodeServerInfo = Schema6.decodeUnknownOption(ServerInfo);
|
|
625
|
+
var isListToolsResult = (value) => decodeListToolsResult(value)._tag === "Some";
|
|
626
|
+
var sanitize = (value) => {
|
|
627
|
+
const s = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
628
|
+
return s || "tool";
|
|
629
|
+
};
|
|
630
|
+
var uniqueId = (value, seen) => {
|
|
631
|
+
const base = sanitize(value);
|
|
632
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
633
|
+
seen.set(base, n);
|
|
634
|
+
return n === 1 ? base : `${base}_${n}`;
|
|
635
|
+
};
|
|
636
|
+
var extractManifestFromListToolsResult = (listToolsResult, metadata) => {
|
|
637
|
+
const seen = /* @__PURE__ */ new Map();
|
|
638
|
+
const listed = decodeListToolsResult(listToolsResult).pipe(
|
|
639
|
+
(opt) => opt._tag === "Some" ? opt.value.tools : []
|
|
640
|
+
);
|
|
641
|
+
const server = decodeServerInfo(metadata?.serverInfo).pipe(
|
|
642
|
+
(opt) => opt._tag === "Some" ? { name: opt.value.name ?? null, version: opt.value.version ?? null } : null
|
|
643
|
+
);
|
|
644
|
+
const tools = listed.flatMap((tool) => {
|
|
645
|
+
const toolName = tool.name.trim();
|
|
646
|
+
if (!toolName) return [];
|
|
647
|
+
return [
|
|
648
|
+
{
|
|
649
|
+
toolId: uniqueId(toolName, seen),
|
|
650
|
+
toolName,
|
|
651
|
+
description: tool.description ?? null,
|
|
652
|
+
inputSchema: tool.inputSchema ?? tool.parameters,
|
|
653
|
+
outputSchema: tool.outputSchema,
|
|
654
|
+
annotations: tool.annotations
|
|
655
|
+
}
|
|
656
|
+
];
|
|
657
|
+
});
|
|
658
|
+
return { server, tools };
|
|
659
|
+
};
|
|
660
|
+
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
661
|
+
var hostnameOf = (url) => {
|
|
662
|
+
try {
|
|
663
|
+
return new URL(url).hostname;
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
var basenameOf = (path) => path.trim().split(/[\\/]/).pop() ?? path.trim();
|
|
669
|
+
var deriveMcpNamespace = (input) => {
|
|
670
|
+
if (input.name?.trim()) return slugify(input.name) || "mcp";
|
|
671
|
+
const fromEndpoint = input.endpoint?.trim() ? hostnameOf(input.endpoint) : null;
|
|
672
|
+
if (fromEndpoint) return slugify(fromEndpoint) || "mcp";
|
|
673
|
+
if (input.command?.trim()) return slugify(basenameOf(input.command)) || "mcp";
|
|
674
|
+
return "mcp";
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// src/sdk/discover.ts
|
|
678
|
+
var discoverTools = (connector) => Effect5.gen(function* () {
|
|
679
|
+
const connection = yield* connector.pipe(
|
|
680
|
+
Effect5.mapError(
|
|
681
|
+
(err) => new McpToolDiscoveryError({
|
|
682
|
+
stage: "connect",
|
|
683
|
+
message: `Failed connecting to MCP server: ${err.message}`
|
|
684
|
+
})
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
const listResult = yield* Effect5.tryPromise({
|
|
688
|
+
try: () => connection.client.listTools(),
|
|
689
|
+
catch: (cause) => new McpToolDiscoveryError({
|
|
690
|
+
stage: "list_tools",
|
|
691
|
+
message: `Failed listing MCP tools: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
692
|
+
})
|
|
693
|
+
});
|
|
694
|
+
if (!isListToolsResult(listResult)) {
|
|
695
|
+
yield* Effect5.promise(() => connection.close().catch(() => {
|
|
696
|
+
}));
|
|
697
|
+
return yield* Effect5.fail(
|
|
698
|
+
new McpToolDiscoveryError({
|
|
699
|
+
stage: "list_tools",
|
|
700
|
+
message: "MCP listTools response did not match the expected schema"
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const manifest = extractManifestFromListToolsResult(listResult, {
|
|
705
|
+
serverInfo: connection.client.getServerVersion?.()
|
|
706
|
+
});
|
|
707
|
+
yield* Effect5.promise(() => connection.close().catch(() => {
|
|
708
|
+
}));
|
|
709
|
+
return manifest;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// src/sdk/invoke.ts
|
|
713
|
+
import { Cause, Effect as Effect6, Exit, Schema as Schema7, ScopedCache } from "effect";
|
|
714
|
+
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
715
|
+
import {
|
|
716
|
+
FormElicitation,
|
|
717
|
+
UrlElicitation
|
|
718
|
+
} from "@executor-js/sdk/core";
|
|
719
|
+
var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
720
|
+
var connectionCacheKey = (sd, invokerScope) => sd.transport === "stdio" ? `stdio:${sd.command}` : (
|
|
721
|
+
// Remote sources may resolve per-user secrets (OAuth tokens, header
|
|
722
|
+
// auth) via scope shadowing, so two users invoking the same source
|
|
723
|
+
// get different Authorization headers. The connection caches that
|
|
724
|
+
// header in transport state, so the cache key must include the
|
|
725
|
+
// invoking scope — otherwise user B re-uses user A's connection
|
|
726
|
+
// (and user A's tokens).
|
|
727
|
+
`remote:${invokerScope}:${sd.endpoint}`
|
|
728
|
+
);
|
|
729
|
+
var McpElicitParams = Schema7.Union([
|
|
730
|
+
Schema7.Struct({
|
|
731
|
+
mode: Schema7.Literal("url"),
|
|
732
|
+
message: Schema7.String,
|
|
733
|
+
url: Schema7.String,
|
|
734
|
+
elicitationId: Schema7.optional(Schema7.String),
|
|
735
|
+
id: Schema7.optional(Schema7.String)
|
|
736
|
+
}),
|
|
737
|
+
Schema7.Struct({
|
|
738
|
+
mode: Schema7.optional(Schema7.Literal("form")),
|
|
739
|
+
message: Schema7.String,
|
|
740
|
+
requestedSchema: Schema7.Record(Schema7.String, Schema7.Unknown)
|
|
741
|
+
})
|
|
742
|
+
]);
|
|
743
|
+
var decodeElicitParams = Schema7.decodeUnknownSync(McpElicitParams);
|
|
744
|
+
var toElicitationRequest = (params) => params.mode === "url" ? new UrlElicitation({
|
|
745
|
+
message: params.message,
|
|
746
|
+
url: params.url,
|
|
747
|
+
elicitationId: params.elicitationId ?? params.id ?? ""
|
|
748
|
+
}) : new FormElicitation({
|
|
749
|
+
message: params.message,
|
|
750
|
+
requestedSchema: params.requestedSchema
|
|
751
|
+
});
|
|
752
|
+
var installElicitationHandler = (client, elicit) => {
|
|
753
|
+
client.setRequestHandler(
|
|
754
|
+
ElicitRequestSchema,
|
|
755
|
+
async (request) => {
|
|
756
|
+
const params = decodeElicitParams(request.params);
|
|
757
|
+
const req = toElicitationRequest(params);
|
|
758
|
+
const exit = await Effect6.runPromiseExit(elicit(req));
|
|
759
|
+
if (Exit.isSuccess(exit)) {
|
|
760
|
+
const response = exit.value;
|
|
761
|
+
return {
|
|
762
|
+
action: response.action,
|
|
763
|
+
...response.action === "accept" && response.content ? { content: response.content } : {}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const failure = exit.cause.reasons.find(Cause.isFailReason);
|
|
767
|
+
if (failure) {
|
|
768
|
+
const err = failure.error;
|
|
769
|
+
if (err._tag === "ElicitationDeclinedError") {
|
|
770
|
+
return { action: err.action ?? "decline" };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
throw Cause.squash(exit.cause);
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
};
|
|
777
|
+
var useConnection = (connection, toolName, args, elicit) => Effect6.gen(function* () {
|
|
778
|
+
installElicitationHandler(connection.client, elicit);
|
|
779
|
+
return yield* Effect6.tryPromise({
|
|
780
|
+
try: () => connection.client.callTool({ name: toolName, arguments: args }),
|
|
781
|
+
catch: (cause) => new McpInvocationError({
|
|
782
|
+
toolName,
|
|
783
|
+
message: `MCP tool call failed for ${toolName}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
784
|
+
})
|
|
785
|
+
}).pipe(
|
|
786
|
+
Effect6.withSpan("plugin.mcp.client.call_tool", {
|
|
787
|
+
attributes: { "mcp.tool.name": toolName }
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
var invokeMcpTool = (input) => {
|
|
792
|
+
const transport = input.sourceData.transport === "stdio" ? "stdio" : input.sourceData.remoteTransport ?? "auto";
|
|
793
|
+
return Effect6.gen(function* () {
|
|
794
|
+
const cacheKey = connectionCacheKey(input.sourceData, input.invokerScope);
|
|
795
|
+
const args = asRecord(input.args);
|
|
796
|
+
const connector = input.resolveConnector();
|
|
797
|
+
input.pendingConnectors.set(cacheKey, connector);
|
|
798
|
+
const cacheHit = yield* ScopedCache.has(input.connectionCache, cacheKey);
|
|
799
|
+
const firstConnection = yield* ScopedCache.get(input.connectionCache, cacheKey).pipe(
|
|
800
|
+
Effect6.withSpan("plugin.mcp.connection.acquire", {
|
|
801
|
+
attributes: {
|
|
802
|
+
"plugin.mcp.transport": transport,
|
|
803
|
+
"plugin.mcp.cache_key": cacheKey,
|
|
804
|
+
"plugin.mcp.attempt": 1,
|
|
805
|
+
"plugin.mcp.cache_hit": cacheHit
|
|
806
|
+
}
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
return yield* useConnection(
|
|
810
|
+
firstConnection,
|
|
811
|
+
input.toolName,
|
|
812
|
+
args,
|
|
813
|
+
input.elicit
|
|
814
|
+
).pipe(
|
|
815
|
+
// On failure, invalidate the cache and retry once with a fresh
|
|
816
|
+
// connection. Matches the old invoker's retry-once semantics.
|
|
817
|
+
Effect6.catch(
|
|
818
|
+
() => Effect6.gen(function* () {
|
|
819
|
+
yield* ScopedCache.invalidate(input.connectionCache, cacheKey);
|
|
820
|
+
input.pendingConnectors.set(cacheKey, connector);
|
|
821
|
+
const fresh = yield* ScopedCache.get(input.connectionCache, cacheKey);
|
|
822
|
+
return yield* useConnection(
|
|
823
|
+
fresh,
|
|
824
|
+
input.toolName,
|
|
825
|
+
args,
|
|
826
|
+
input.elicit
|
|
827
|
+
);
|
|
828
|
+
}).pipe(
|
|
829
|
+
Effect6.withSpan("plugin.mcp.invoke.retry", {
|
|
830
|
+
attributes: {
|
|
831
|
+
"plugin.mcp.transport": transport,
|
|
832
|
+
"plugin.mcp.cache_key": cacheKey,
|
|
833
|
+
"mcp.tool.name": input.toolName
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
}).pipe(
|
|
840
|
+
Effect6.scoped,
|
|
841
|
+
Effect6.withSpan("plugin.mcp.invoke", {
|
|
842
|
+
attributes: {
|
|
843
|
+
"mcp.tool.name": input.toolName,
|
|
844
|
+
"plugin.mcp.tool_id": input.toolId,
|
|
845
|
+
"plugin.mcp.transport": transport
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/sdk/probe-shape.ts
|
|
852
|
+
import { Effect as Effect7 } from "effect";
|
|
853
|
+
var INITIALIZE_BODY = JSON.stringify({
|
|
854
|
+
jsonrpc: "2.0",
|
|
855
|
+
id: 1,
|
|
856
|
+
method: "initialize",
|
|
857
|
+
params: {
|
|
858
|
+
protocolVersion: "2025-06-18",
|
|
859
|
+
capabilities: {},
|
|
860
|
+
clientInfo: { name: "executor-probe", version: "0" }
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
var readHeader = (headers, name) => {
|
|
864
|
+
const direct = headers.get(name);
|
|
865
|
+
if (direct !== null) return direct;
|
|
866
|
+
const lower = name.toLowerCase();
|
|
867
|
+
for (const [k, v] of headers) {
|
|
868
|
+
if (k.toLowerCase() === lower) return v;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
};
|
|
872
|
+
var probeMcpEndpointShape = (endpoint, options = {}) => Effect7.gen(function* () {
|
|
873
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
874
|
+
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
875
|
+
const outcome = yield* Effect7.tryPromise({
|
|
876
|
+
try: async () => {
|
|
877
|
+
const controller = new AbortController();
|
|
878
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
879
|
+
try {
|
|
880
|
+
const classify = (response, method) => {
|
|
881
|
+
if (response.status === 401) {
|
|
882
|
+
const wwwAuth = readHeader(response.headers, "www-authenticate");
|
|
883
|
+
if (wwwAuth && /^\s*bearer\b/i.test(wwwAuth)) {
|
|
884
|
+
return { kind: "mcp", requiresAuth: true };
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
kind: "not-mcp",
|
|
888
|
+
reason: "401 without Bearer WWW-Authenticate \u2014 not an MCP auth challenge"
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (response.status >= 200 && response.status < 300) {
|
|
892
|
+
if (method === "GET") {
|
|
893
|
+
const contentType = readHeader(response.headers, "content-type") ?? "";
|
|
894
|
+
if (!/^\s*text\/event-stream\b/i.test(contentType)) {
|
|
895
|
+
return {
|
|
896
|
+
kind: "not-mcp",
|
|
897
|
+
reason: "GET response is not an SSE stream"
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return { kind: "mcp", requiresAuth: false };
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
};
|
|
905
|
+
const url = new URL(endpoint);
|
|
906
|
+
for (const [key, value] of Object.entries(options.queryParams ?? {})) {
|
|
907
|
+
url.searchParams.set(key, value);
|
|
908
|
+
}
|
|
909
|
+
const authHeaders = options.headers ?? {};
|
|
910
|
+
const postResponse = await fetchImpl(url, {
|
|
911
|
+
method: "POST",
|
|
912
|
+
headers: {
|
|
913
|
+
...authHeaders,
|
|
914
|
+
"content-type": "application/json",
|
|
915
|
+
accept: "application/json, text/event-stream"
|
|
916
|
+
},
|
|
917
|
+
body: INITIALIZE_BODY,
|
|
918
|
+
signal: controller.signal
|
|
919
|
+
});
|
|
920
|
+
const postResult = classify(postResponse, "POST");
|
|
921
|
+
if (postResult) return postResult;
|
|
922
|
+
if ([404, 405, 406, 415].includes(postResponse.status)) {
|
|
923
|
+
const getResponse = await fetchImpl(url, {
|
|
924
|
+
method: "GET",
|
|
925
|
+
headers: { ...authHeaders, accept: "text/event-stream" },
|
|
926
|
+
signal: controller.signal
|
|
927
|
+
});
|
|
928
|
+
const getResult = classify(getResponse, "GET");
|
|
929
|
+
if (getResult) return getResult;
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
kind: "not-mcp",
|
|
933
|
+
reason: `unexpected status ${postResponse.status} for initialize`
|
|
934
|
+
};
|
|
935
|
+
} finally {
|
|
936
|
+
clearTimeout(timer);
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
catch: (cause) => cause
|
|
940
|
+
}).pipe(
|
|
941
|
+
Effect7.catch(
|
|
942
|
+
(cause) => Effect7.succeed({
|
|
943
|
+
kind: "unreachable",
|
|
944
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
945
|
+
})
|
|
946
|
+
)
|
|
947
|
+
);
|
|
948
|
+
return outcome;
|
|
949
|
+
}).pipe(Effect7.withSpan("mcp.plugin.probe_shape"));
|
|
950
|
+
|
|
951
|
+
// src/sdk/plugin.ts
|
|
952
|
+
import {
|
|
953
|
+
SECRET_REF_PREFIX
|
|
954
|
+
} from "@executor-js/config";
|
|
955
|
+
var toStoredSourceData = (config) => {
|
|
956
|
+
if (config.transport === "stdio") {
|
|
957
|
+
return {
|
|
958
|
+
transport: "stdio",
|
|
959
|
+
command: config.command,
|
|
960
|
+
args: config.args,
|
|
961
|
+
env: config.env,
|
|
962
|
+
cwd: config.cwd
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
transport: "remote",
|
|
967
|
+
endpoint: config.endpoint,
|
|
968
|
+
remoteTransport: config.remoteTransport ?? "auto",
|
|
969
|
+
queryParams: config.queryParams,
|
|
970
|
+
headers: config.headers,
|
|
971
|
+
auth: config.auth ?? { kind: "none" }
|
|
972
|
+
};
|
|
973
|
+
};
|
|
974
|
+
var normalizeNamespace = (config) => config.namespace ?? deriveMcpNamespace({
|
|
975
|
+
name: config.name,
|
|
976
|
+
endpoint: config.transport === "remote" ? config.endpoint : void 0,
|
|
977
|
+
command: config.transport === "stdio" ? config.command : void 0
|
|
978
|
+
});
|
|
979
|
+
var toBinding = (entry) => new McpToolBinding({
|
|
980
|
+
toolId: entry.toolId,
|
|
981
|
+
toolName: entry.toolName,
|
|
982
|
+
description: entry.description,
|
|
983
|
+
inputSchema: entry.inputSchema,
|
|
984
|
+
outputSchema: entry.outputSchema,
|
|
985
|
+
annotations: entry.annotations
|
|
986
|
+
});
|
|
987
|
+
var makeOAuthProvider = (accessToken) => ({
|
|
988
|
+
get redirectUrl() {
|
|
989
|
+
return "http://localhost/oauth/callback";
|
|
990
|
+
},
|
|
991
|
+
get clientMetadata() {
|
|
992
|
+
return {
|
|
993
|
+
redirect_uris: ["http://localhost/oauth/callback"],
|
|
994
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
995
|
+
response_types: ["code"],
|
|
996
|
+
token_endpoint_auth_method: "none",
|
|
997
|
+
client_name: "Executor"
|
|
998
|
+
};
|
|
999
|
+
},
|
|
1000
|
+
clientInformation: () => void 0,
|
|
1001
|
+
saveClientInformation: () => void 0,
|
|
1002
|
+
tokens: () => ({ access_token: accessToken, token_type: "Bearer" }),
|
|
1003
|
+
saveTokens: () => void 0,
|
|
1004
|
+
redirectToAuthorization: async () => {
|
|
1005
|
+
throw new Error("MCP OAuth re-authorization required");
|
|
1006
|
+
},
|
|
1007
|
+
saveCodeVerifier: () => void 0,
|
|
1008
|
+
codeVerifier: () => {
|
|
1009
|
+
throw new Error("No active PKCE verifier");
|
|
1010
|
+
},
|
|
1011
|
+
saveDiscoveryState: () => void 0,
|
|
1012
|
+
discoveryState: () => void 0
|
|
1013
|
+
});
|
|
1014
|
+
var remoteConnectionError = (message) => new McpConnectionError({ transport: "remote", message });
|
|
1015
|
+
var mcpDiscoveryError = (message) => new McpToolDiscoveryError({ stage: "list_tools", message });
|
|
1016
|
+
var resolveSecretBackedMap = (values, ctx) => resolveSharedSecretBackedMap({
|
|
1017
|
+
values,
|
|
1018
|
+
getSecret: ctx.secrets.get,
|
|
1019
|
+
onMissing: (_name, value) => remoteConnectionError(`Failed to resolve secret "${value.secretId}"`),
|
|
1020
|
+
onError: (err, _name, value) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? remoteConnectionError(`Failed to resolve secret "${value.secretId}"`) : err
|
|
1021
|
+
}).pipe(
|
|
1022
|
+
Effect8.mapError(
|
|
1023
|
+
(err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? remoteConnectionError("Failed to resolve secret") : err
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
1026
|
+
var plainStringMap = (values) => {
|
|
1027
|
+
if (!values) return void 0;
|
|
1028
|
+
const entries = Object.entries(values).filter(
|
|
1029
|
+
(entry) => typeof entry[1] === "string"
|
|
1030
|
+
);
|
|
1031
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
1032
|
+
};
|
|
1033
|
+
var resolveConnectorInput = (sd, ctx, allowStdio) => {
|
|
1034
|
+
if (sd.transport === "stdio") {
|
|
1035
|
+
if (!allowStdio) {
|
|
1036
|
+
return Effect8.fail(
|
|
1037
|
+
new McpConnectionError({
|
|
1038
|
+
transport: "stdio",
|
|
1039
|
+
message: "MCP stdio transport is disabled. Enable it by passing `dangerouslyAllowStdioMCP: true` to mcpPlugin() \u2014 only safe for trusted local contexts."
|
|
1040
|
+
})
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
return Effect8.succeed({
|
|
1044
|
+
transport: "stdio",
|
|
1045
|
+
command: sd.command,
|
|
1046
|
+
args: sd.args,
|
|
1047
|
+
env: sd.env,
|
|
1048
|
+
cwd: sd.cwd
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
return Effect8.gen(function* () {
|
|
1052
|
+
const resolvedHeaders = yield* resolveSecretBackedMap(sd.headers, ctx);
|
|
1053
|
+
const resolvedQueryParams = yield* resolveSecretBackedMap(sd.queryParams, ctx);
|
|
1054
|
+
const headers = { ...resolvedHeaders ?? {} };
|
|
1055
|
+
let authProvider;
|
|
1056
|
+
const auth = sd.auth;
|
|
1057
|
+
if (auth.kind === "header") {
|
|
1058
|
+
const val = yield* ctx.secrets.get(auth.secretId).pipe(
|
|
1059
|
+
Effect8.mapError(
|
|
1060
|
+
(err) => "_tag" in err && err._tag === "SecretOwnedByConnectionError" ? remoteConnectionError(`Failed to resolve secret "${auth.secretId}"`) : err
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
if (val === null) {
|
|
1064
|
+
return yield* Effect8.fail(
|
|
1065
|
+
remoteConnectionError(`Failed to resolve secret "${auth.secretId}"`)
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
headers[auth.headerName] = auth.prefix ? `${auth.prefix}${val}` : val;
|
|
1069
|
+
} else if (auth.kind === "oauth2") {
|
|
1070
|
+
const accessToken = yield* ctx.connections.accessToken(auth.connectionId).pipe(
|
|
1071
|
+
Effect8.mapError(
|
|
1072
|
+
(err) => remoteConnectionError(
|
|
1073
|
+
`Failed to resolve OAuth connection "${auth.connectionId}": ${"message" in err ? err.message : String(err)}`
|
|
1074
|
+
)
|
|
1075
|
+
)
|
|
1076
|
+
);
|
|
1077
|
+
authProvider = makeOAuthProvider(accessToken);
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
transport: "remote",
|
|
1081
|
+
endpoint: sd.endpoint,
|
|
1082
|
+
remoteTransport: sd.remoteTransport,
|
|
1083
|
+
queryParams: resolvedQueryParams,
|
|
1084
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0,
|
|
1085
|
+
authProvider
|
|
1086
|
+
};
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
var makeRuntime = () => Effect8.gen(function* () {
|
|
1090
|
+
const cacheScope = yield* Scope.make();
|
|
1091
|
+
const pendingConnectors = /* @__PURE__ */ new Map();
|
|
1092
|
+
const connectionCache = yield* ScopedCache2.make({
|
|
1093
|
+
lookup: (key) => Effect8.acquireRelease(
|
|
1094
|
+
Effect8.suspend(() => {
|
|
1095
|
+
const connector = pendingConnectors.get(key);
|
|
1096
|
+
if (!connector) {
|
|
1097
|
+
return Effect8.fail(
|
|
1098
|
+
new McpConnectionError({
|
|
1099
|
+
transport: "auto",
|
|
1100
|
+
message: `No pending connector for key: ${key}`
|
|
1101
|
+
})
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return connector;
|
|
1105
|
+
}),
|
|
1106
|
+
(connection) => Effect8.promise(() => connection.close().catch(() => {
|
|
1107
|
+
}))
|
|
1108
|
+
),
|
|
1109
|
+
capacity: 64,
|
|
1110
|
+
timeToLive: Duration.minutes(5)
|
|
1111
|
+
}).pipe(Scope.provide(cacheScope));
|
|
1112
|
+
return { connectionCache, pendingConnectors, cacheScope };
|
|
1113
|
+
});
|
|
1114
|
+
var secretRef = (id) => `${SECRET_REF_PREFIX}${id}`;
|
|
1115
|
+
var authToConfig = (auth) => {
|
|
1116
|
+
if (!auth) return void 0;
|
|
1117
|
+
if (auth.kind === "none") return { kind: "none" };
|
|
1118
|
+
if (auth.kind === "header") {
|
|
1119
|
+
return {
|
|
1120
|
+
kind: "header",
|
|
1121
|
+
headerName: auth.headerName,
|
|
1122
|
+
secret: secretRef(auth.secretId),
|
|
1123
|
+
prefix: auth.prefix
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
kind: "oauth2",
|
|
1128
|
+
connectionId: auth.connectionId
|
|
1129
|
+
};
|
|
1130
|
+
};
|
|
1131
|
+
var toMcpConfigEntry = (namespace, sourceName, config) => {
|
|
1132
|
+
if (config.transport === "stdio") {
|
|
1133
|
+
const entry2 = {
|
|
1134
|
+
kind: "mcp",
|
|
1135
|
+
transport: "stdio",
|
|
1136
|
+
name: sourceName,
|
|
1137
|
+
command: config.command,
|
|
1138
|
+
args: config.args,
|
|
1139
|
+
env: config.env,
|
|
1140
|
+
cwd: config.cwd,
|
|
1141
|
+
namespace
|
|
1142
|
+
};
|
|
1143
|
+
return entry2;
|
|
1144
|
+
}
|
|
1145
|
+
const entry = {
|
|
1146
|
+
kind: "mcp",
|
|
1147
|
+
transport: "remote",
|
|
1148
|
+
name: sourceName,
|
|
1149
|
+
endpoint: config.endpoint,
|
|
1150
|
+
remoteTransport: config.remoteTransport,
|
|
1151
|
+
queryParams: plainStringMap(config.queryParams),
|
|
1152
|
+
headers: plainStringMap(config.headers),
|
|
1153
|
+
namespace,
|
|
1154
|
+
auth: authToConfig(config.auth)
|
|
1155
|
+
};
|
|
1156
|
+
return entry;
|
|
1157
|
+
};
|
|
1158
|
+
var mcpPlugin = definePlugin((options) => {
|
|
1159
|
+
const allowStdio = options?.dangerouslyAllowStdioMCP ?? false;
|
|
1160
|
+
const runtimeRef = { current: null };
|
|
1161
|
+
const ensureRuntime = () => runtimeRef.current ? Effect8.succeed(runtimeRef.current) : makeRuntime().pipe(
|
|
1162
|
+
Effect8.tap(
|
|
1163
|
+
(rt) => Effect8.sync(() => {
|
|
1164
|
+
runtimeRef.current = rt;
|
|
1165
|
+
})
|
|
1166
|
+
)
|
|
1167
|
+
);
|
|
1168
|
+
return {
|
|
1169
|
+
id: "mcp",
|
|
1170
|
+
packageName: "@executor-js/plugin-mcp",
|
|
1171
|
+
schema: mcpSchema,
|
|
1172
|
+
storage: (deps) => makeMcpStore(deps),
|
|
1173
|
+
extension: (ctx) => {
|
|
1174
|
+
const probeEndpoint = (input) => Effect8.gen(function* () {
|
|
1175
|
+
const endpoint = typeof input === "string" ? input : input.endpoint;
|
|
1176
|
+
const trimmed = endpoint.trim();
|
|
1177
|
+
if (!trimmed) {
|
|
1178
|
+
return yield* Effect8.fail(remoteConnectionError("Endpoint URL is required"));
|
|
1179
|
+
}
|
|
1180
|
+
const name = yield* Effect8.try({
|
|
1181
|
+
try: () => new URL(trimmed).hostname,
|
|
1182
|
+
catch: () => "mcp"
|
|
1183
|
+
}).pipe(
|
|
1184
|
+
Effect8.orElseSucceed(() => "mcp")
|
|
1185
|
+
);
|
|
1186
|
+
const namespace = deriveMcpNamespace({ endpoint: trimmed });
|
|
1187
|
+
const probeHeaders = typeof input === "string" ? void 0 : yield* resolveSecretBackedMap(input.headers, ctx);
|
|
1188
|
+
const probeQueryParams = typeof input === "string" ? void 0 : yield* resolveSecretBackedMap(input.queryParams, ctx);
|
|
1189
|
+
const connector = createMcpConnector({
|
|
1190
|
+
transport: "remote",
|
|
1191
|
+
endpoint: trimmed,
|
|
1192
|
+
headers: probeHeaders,
|
|
1193
|
+
queryParams: probeQueryParams
|
|
1194
|
+
});
|
|
1195
|
+
const result = yield* discoverTools(connector).pipe(
|
|
1196
|
+
Effect8.map((m) => ({ ok: true, manifest: m })),
|
|
1197
|
+
Effect8.catch(() => Effect8.succeed({ ok: false, manifest: null })),
|
|
1198
|
+
Effect8.withSpan("mcp.plugin.discover_tools")
|
|
1199
|
+
);
|
|
1200
|
+
if (result.ok && result.manifest) {
|
|
1201
|
+
return {
|
|
1202
|
+
connected: true,
|
|
1203
|
+
requiresOAuth: false,
|
|
1204
|
+
name: result.manifest.server?.name ?? name,
|
|
1205
|
+
namespace,
|
|
1206
|
+
toolCount: result.manifest.tools.length,
|
|
1207
|
+
serverName: result.manifest.server?.name ?? null
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const shape = yield* probeMcpEndpointShape(trimmed, {
|
|
1211
|
+
headers: probeHeaders,
|
|
1212
|
+
queryParams: probeQueryParams
|
|
1213
|
+
});
|
|
1214
|
+
if (shape.kind !== "mcp") {
|
|
1215
|
+
return yield* Effect8.fail(
|
|
1216
|
+
remoteConnectionError(
|
|
1217
|
+
shape.kind === "not-mcp" ? `Endpoint does not look like an MCP server: ${shape.reason}` : `Could not reach endpoint: ${shape.reason}`
|
|
1218
|
+
)
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const probeResult = yield* ctx.oauth.probe({
|
|
1222
|
+
endpoint: trimmed,
|
|
1223
|
+
headers: probeHeaders,
|
|
1224
|
+
queryParams: probeQueryParams
|
|
1225
|
+
}).pipe(
|
|
1226
|
+
Effect8.map(() => true),
|
|
1227
|
+
Effect8.catch(() => Effect8.succeed(false)),
|
|
1228
|
+
Effect8.withSpan("mcp.plugin.probe_oauth")
|
|
1229
|
+
);
|
|
1230
|
+
if (probeResult) {
|
|
1231
|
+
return {
|
|
1232
|
+
connected: false,
|
|
1233
|
+
requiresOAuth: true,
|
|
1234
|
+
name,
|
|
1235
|
+
namespace,
|
|
1236
|
+
toolCount: null,
|
|
1237
|
+
serverName: null
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
return yield* Effect8.fail(
|
|
1241
|
+
remoteConnectionError("MCP server requires authentication but OAuth discovery failed")
|
|
1242
|
+
);
|
|
1243
|
+
}).pipe(
|
|
1244
|
+
Effect8.withSpan("mcp.plugin.probe_endpoint", {
|
|
1245
|
+
attributes: { "mcp.endpoint": typeof input === "string" ? input : input.endpoint }
|
|
1246
|
+
})
|
|
1247
|
+
);
|
|
1248
|
+
const configFile = options?.configFile;
|
|
1249
|
+
const addSource = (config) => Effect8.gen(function* () {
|
|
1250
|
+
const namespace = normalizeNamespace(config);
|
|
1251
|
+
const sd = toStoredSourceData(config);
|
|
1252
|
+
const resolved = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
|
|
1253
|
+
Effect8.result,
|
|
1254
|
+
Effect8.withSpan("mcp.plugin.resolve_connector", {
|
|
1255
|
+
attributes: {
|
|
1256
|
+
"mcp.source.namespace": namespace,
|
|
1257
|
+
"mcp.source.transport": sd.transport
|
|
1258
|
+
}
|
|
1259
|
+
})
|
|
1260
|
+
);
|
|
1261
|
+
if (Result.isFailure(resolved) && sd.transport === "stdio") {
|
|
1262
|
+
return yield* Effect8.fail(resolved.failure);
|
|
1263
|
+
}
|
|
1264
|
+
const discovery = Result.isSuccess(resolved) ? yield* discoverTools(createMcpConnector(resolved.success)).pipe(
|
|
1265
|
+
Effect8.mapError(
|
|
1266
|
+
(err) => mcpDiscoveryError(`MCP discovery failed: ${err.message}`)
|
|
1267
|
+
),
|
|
1268
|
+
Effect8.result,
|
|
1269
|
+
Effect8.withSpan("mcp.plugin.discover_tools", {
|
|
1270
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1271
|
+
})
|
|
1272
|
+
) : Result.fail(resolved.failure);
|
|
1273
|
+
const manifest = Result.isSuccess(discovery) ? discovery.success : { server: void 0, tools: [] };
|
|
1274
|
+
const sourceName = config.name ?? manifest.server?.name ?? namespace;
|
|
1275
|
+
yield* ctx.transaction(
|
|
1276
|
+
Effect8.gen(function* () {
|
|
1277
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, config.scope);
|
|
1278
|
+
yield* ctx.storage.removeSource(namespace, config.scope);
|
|
1279
|
+
yield* ctx.storage.putSource({
|
|
1280
|
+
namespace,
|
|
1281
|
+
scope: config.scope,
|
|
1282
|
+
name: sourceName,
|
|
1283
|
+
config: sd
|
|
1284
|
+
});
|
|
1285
|
+
yield* ctx.storage.putBindings(
|
|
1286
|
+
namespace,
|
|
1287
|
+
config.scope,
|
|
1288
|
+
manifest.tools.map((e) => ({
|
|
1289
|
+
toolId: `${namespace}.${e.toolId}`,
|
|
1290
|
+
binding: toBinding(e)
|
|
1291
|
+
}))
|
|
1292
|
+
);
|
|
1293
|
+
yield* ctx.core.sources.register({
|
|
1294
|
+
id: namespace,
|
|
1295
|
+
scope: config.scope,
|
|
1296
|
+
kind: "mcp",
|
|
1297
|
+
name: sourceName,
|
|
1298
|
+
url: sd.transport === "remote" ? sd.endpoint : void 0,
|
|
1299
|
+
canRemove: true,
|
|
1300
|
+
canRefresh: true,
|
|
1301
|
+
canEdit: sd.transport === "remote",
|
|
1302
|
+
tools: manifest.tools.map((e) => ({
|
|
1303
|
+
name: e.toolId,
|
|
1304
|
+
description: e.description ?? `MCP tool: ${e.toolName}`,
|
|
1305
|
+
inputSchema: e.inputSchema,
|
|
1306
|
+
outputSchema: e.outputSchema
|
|
1307
|
+
}))
|
|
1308
|
+
});
|
|
1309
|
+
})
|
|
1310
|
+
).pipe(
|
|
1311
|
+
Effect8.withSpan("mcp.plugin.persist_source", {
|
|
1312
|
+
attributes: {
|
|
1313
|
+
"mcp.source.namespace": namespace,
|
|
1314
|
+
"mcp.source.tool_count": manifest.tools.length
|
|
1315
|
+
}
|
|
1316
|
+
})
|
|
1317
|
+
);
|
|
1318
|
+
if (configFile) {
|
|
1319
|
+
yield* configFile.upsertSource(toMcpConfigEntry(namespace, sourceName, config)).pipe(Effect8.withSpan("mcp.plugin.config_file.upsert"));
|
|
1320
|
+
}
|
|
1321
|
+
if (Result.isFailure(discovery)) {
|
|
1322
|
+
return yield* Effect8.fail(discovery.failure);
|
|
1323
|
+
}
|
|
1324
|
+
return { toolCount: manifest.tools.length, namespace };
|
|
1325
|
+
}).pipe(
|
|
1326
|
+
Effect8.withSpan("mcp.plugin.add_source", {
|
|
1327
|
+
attributes: {
|
|
1328
|
+
"mcp.source.transport": config.transport,
|
|
1329
|
+
"mcp.source.name": config.name
|
|
1330
|
+
}
|
|
1331
|
+
})
|
|
1332
|
+
);
|
|
1333
|
+
const removeSource = (namespace, scope) => Effect8.gen(function* () {
|
|
1334
|
+
yield* ctx.transaction(
|
|
1335
|
+
Effect8.gen(function* () {
|
|
1336
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, scope);
|
|
1337
|
+
yield* ctx.storage.removeSource(namespace, scope);
|
|
1338
|
+
yield* ctx.core.sources.unregister(namespace);
|
|
1339
|
+
})
|
|
1340
|
+
).pipe(Effect8.withSpan("mcp.plugin.persist_remove"));
|
|
1341
|
+
if (configFile) {
|
|
1342
|
+
yield* configFile.removeSource(namespace).pipe(Effect8.withSpan("mcp.plugin.config_file.remove"));
|
|
1343
|
+
}
|
|
1344
|
+
}).pipe(
|
|
1345
|
+
Effect8.withSpan("mcp.plugin.remove_source", {
|
|
1346
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
1349
|
+
const refreshSource = (namespace, scope) => Effect8.gen(function* () {
|
|
1350
|
+
const sd = yield* ctx.storage.getSourceConfig(namespace, scope).pipe(
|
|
1351
|
+
Effect8.withSpan("mcp.plugin.load_source_config", {
|
|
1352
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1353
|
+
})
|
|
1354
|
+
);
|
|
1355
|
+
if (!sd) {
|
|
1356
|
+
return yield* Effect8.fail(
|
|
1357
|
+
remoteConnectionError(`No stored config for MCP source "${namespace}"`)
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const ci = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
|
|
1361
|
+
Effect8.withSpan("mcp.plugin.resolve_connector", {
|
|
1362
|
+
attributes: {
|
|
1363
|
+
"mcp.source.namespace": namespace,
|
|
1364
|
+
"mcp.source.transport": sd.transport
|
|
1365
|
+
}
|
|
1366
|
+
})
|
|
1367
|
+
);
|
|
1368
|
+
const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(
|
|
1369
|
+
Effect8.mapError((err) => mcpDiscoveryError(`MCP refresh failed: ${err.message}`)),
|
|
1370
|
+
Effect8.withSpan("mcp.plugin.discover_tools", {
|
|
1371
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1372
|
+
})
|
|
1373
|
+
);
|
|
1374
|
+
const existing = yield* ctx.storage.getSource(namespace, scope);
|
|
1375
|
+
const sourceName = manifest.server?.name ?? existing?.name ?? namespace;
|
|
1376
|
+
yield* ctx.transaction(
|
|
1377
|
+
Effect8.gen(function* () {
|
|
1378
|
+
yield* ctx.storage.removeBindingsByNamespace(namespace, scope);
|
|
1379
|
+
yield* ctx.core.sources.unregister(namespace);
|
|
1380
|
+
yield* ctx.storage.putBindings(
|
|
1381
|
+
namespace,
|
|
1382
|
+
scope,
|
|
1383
|
+
manifest.tools.map((e) => ({
|
|
1384
|
+
toolId: `${namespace}.${e.toolId}`,
|
|
1385
|
+
binding: toBinding(e)
|
|
1386
|
+
}))
|
|
1387
|
+
);
|
|
1388
|
+
yield* ctx.core.sources.register({
|
|
1389
|
+
id: namespace,
|
|
1390
|
+
scope,
|
|
1391
|
+
kind: "mcp",
|
|
1392
|
+
name: sourceName,
|
|
1393
|
+
url: sd.transport === "remote" ? sd.endpoint : void 0,
|
|
1394
|
+
canRemove: true,
|
|
1395
|
+
canRefresh: true,
|
|
1396
|
+
canEdit: sd.transport === "remote",
|
|
1397
|
+
tools: manifest.tools.map((e) => ({
|
|
1398
|
+
name: e.toolId,
|
|
1399
|
+
description: e.description ?? `MCP tool: ${e.toolName}`,
|
|
1400
|
+
inputSchema: e.inputSchema,
|
|
1401
|
+
outputSchema: e.outputSchema
|
|
1402
|
+
}))
|
|
1403
|
+
});
|
|
1404
|
+
})
|
|
1405
|
+
).pipe(
|
|
1406
|
+
Effect8.withSpan("mcp.plugin.persist_source", {
|
|
1407
|
+
attributes: {
|
|
1408
|
+
"mcp.source.namespace": namespace,
|
|
1409
|
+
"mcp.source.tool_count": manifest.tools.length
|
|
1410
|
+
}
|
|
1411
|
+
})
|
|
1412
|
+
);
|
|
1413
|
+
return { toolCount: manifest.tools.length };
|
|
1414
|
+
}).pipe(
|
|
1415
|
+
Effect8.withSpan("mcp.plugin.refresh_source", {
|
|
1416
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1417
|
+
})
|
|
1418
|
+
);
|
|
1419
|
+
const updateSource = (namespace, scope, input) => Effect8.gen(function* () {
|
|
1420
|
+
const existing = yield* ctx.storage.getSource(namespace, scope);
|
|
1421
|
+
if (!existing || existing.config.transport !== "remote") return;
|
|
1422
|
+
const remote = existing.config;
|
|
1423
|
+
const updatedConfig = {
|
|
1424
|
+
...remote,
|
|
1425
|
+
...input.endpoint !== void 0 ? { endpoint: input.endpoint } : {},
|
|
1426
|
+
...input.headers !== void 0 ? { headers: input.headers } : {},
|
|
1427
|
+
...input.auth !== void 0 ? { auth: input.auth } : {},
|
|
1428
|
+
...input.queryParams !== void 0 ? { queryParams: input.queryParams } : {}
|
|
1429
|
+
};
|
|
1430
|
+
yield* ctx.storage.putSource({
|
|
1431
|
+
namespace,
|
|
1432
|
+
scope,
|
|
1433
|
+
name: input.name?.trim() || existing.name,
|
|
1434
|
+
config: updatedConfig
|
|
1435
|
+
});
|
|
1436
|
+
}).pipe(
|
|
1437
|
+
Effect8.withSpan("mcp.plugin.update_source", {
|
|
1438
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1439
|
+
})
|
|
1440
|
+
);
|
|
1441
|
+
const getSource = (namespace, scope) => ctx.storage.getSource(namespace, scope).pipe(
|
|
1442
|
+
Effect8.withSpan("mcp.plugin.get_source", {
|
|
1443
|
+
attributes: { "mcp.source.namespace": namespace }
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
return {
|
|
1447
|
+
probeEndpoint,
|
|
1448
|
+
addSource,
|
|
1449
|
+
removeSource,
|
|
1450
|
+
refreshSource,
|
|
1451
|
+
getSource,
|
|
1452
|
+
updateSource
|
|
1453
|
+
};
|
|
1454
|
+
},
|
|
1455
|
+
invokeTool: ({ ctx, toolRow, args, elicit }) => Effect8.gen(function* () {
|
|
1456
|
+
const runtime = yield* ensureRuntime();
|
|
1457
|
+
const toolScope = toolRow.scope_id;
|
|
1458
|
+
const entry = yield* ctx.storage.getBinding(toolRow.id, toolScope).pipe(
|
|
1459
|
+
Effect8.withSpan("mcp.plugin.load_binding", {
|
|
1460
|
+
attributes: { "mcp.tool.name": toolRow.id }
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
if (!entry) {
|
|
1464
|
+
return yield* Effect8.fail(new Error(`No MCP binding found for tool "${toolRow.id}"`));
|
|
1465
|
+
}
|
|
1466
|
+
const sd = yield* ctx.storage.getSourceConfig(entry.namespace, toolScope).pipe(
|
|
1467
|
+
Effect8.withSpan("mcp.plugin.load_source_config", {
|
|
1468
|
+
attributes: { "mcp.source.namespace": entry.namespace }
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
if (!sd) {
|
|
1472
|
+
return yield* Effect8.fail(
|
|
1473
|
+
new Error(`No MCP source config for namespace "${entry.namespace}"`)
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
return yield* invokeMcpTool({
|
|
1477
|
+
toolId: toolRow.id,
|
|
1478
|
+
toolName: entry.binding.toolName,
|
|
1479
|
+
args,
|
|
1480
|
+
sourceData: sd,
|
|
1481
|
+
invokerScope: ctx.scopes[0].id,
|
|
1482
|
+
resolveConnector: () => resolveConnectorInput(sd, ctx, allowStdio).pipe(
|
|
1483
|
+
Effect8.flatMap((ci) => createMcpConnector(ci)),
|
|
1484
|
+
Effect8.mapError(
|
|
1485
|
+
(err) => err instanceof McpConnectionError ? err : new McpConnectionError({
|
|
1486
|
+
transport: "auto",
|
|
1487
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1488
|
+
})
|
|
1489
|
+
),
|
|
1490
|
+
Effect8.withSpan("mcp.plugin.resolve_connector", {
|
|
1491
|
+
attributes: {
|
|
1492
|
+
"mcp.source.namespace": entry.namespace,
|
|
1493
|
+
"mcp.source.transport": sd.transport
|
|
1494
|
+
}
|
|
1495
|
+
})
|
|
1496
|
+
),
|
|
1497
|
+
connectionCache: runtime.connectionCache,
|
|
1498
|
+
pendingConnectors: runtime.pendingConnectors,
|
|
1499
|
+
elicit
|
|
1500
|
+
});
|
|
1501
|
+
}).pipe(
|
|
1502
|
+
Effect8.withSpan("mcp.plugin.invoke_tool", {
|
|
1503
|
+
attributes: {
|
|
1504
|
+
"mcp.tool.name": toolRow.id,
|
|
1505
|
+
"mcp.tool.source_id": toolRow.source_id
|
|
1506
|
+
}
|
|
1507
|
+
})
|
|
1508
|
+
),
|
|
1509
|
+
detect: ({ ctx, url }) => Effect8.gen(function* () {
|
|
1510
|
+
const trimmed = url.trim();
|
|
1511
|
+
if (!trimmed) return null;
|
|
1512
|
+
const parsed = yield* Effect8.try({
|
|
1513
|
+
try: () => new URL(trimmed),
|
|
1514
|
+
catch: (cause) => cause
|
|
1515
|
+
}).pipe(Effect8.option);
|
|
1516
|
+
if (parsed._tag === "None") return null;
|
|
1517
|
+
const name = parsed.value.hostname || "mcp";
|
|
1518
|
+
const namespace = deriveMcpNamespace({ endpoint: trimmed });
|
|
1519
|
+
const connector = createMcpConnector({
|
|
1520
|
+
transport: "remote",
|
|
1521
|
+
endpoint: trimmed
|
|
1522
|
+
});
|
|
1523
|
+
const connected = yield* discoverTools(connector).pipe(
|
|
1524
|
+
Effect8.map(() => true),
|
|
1525
|
+
Effect8.catch(() => Effect8.succeed(false)),
|
|
1526
|
+
Effect8.withSpan("mcp.plugin.discover_tools")
|
|
1527
|
+
);
|
|
1528
|
+
if (connected) {
|
|
1529
|
+
return new SourceDetectionResult({
|
|
1530
|
+
kind: "mcp",
|
|
1531
|
+
confidence: "high",
|
|
1532
|
+
endpoint: trimmed,
|
|
1533
|
+
name,
|
|
1534
|
+
namespace
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
const shape = yield* probeMcpEndpointShape(trimmed);
|
|
1538
|
+
if (shape.kind !== "mcp") return null;
|
|
1539
|
+
const probeOk = yield* ctx.oauth.probe({ endpoint: trimmed }).pipe(
|
|
1540
|
+
Effect8.map(() => true),
|
|
1541
|
+
Effect8.catch(() => Effect8.succeed(false)),
|
|
1542
|
+
Effect8.withSpan("mcp.plugin.probe_oauth")
|
|
1543
|
+
);
|
|
1544
|
+
if (!probeOk) return null;
|
|
1545
|
+
return new SourceDetectionResult({
|
|
1546
|
+
kind: "mcp",
|
|
1547
|
+
confidence: "high",
|
|
1548
|
+
endpoint: trimmed,
|
|
1549
|
+
name,
|
|
1550
|
+
namespace
|
|
1551
|
+
});
|
|
1552
|
+
}).pipe(
|
|
1553
|
+
Effect8.catch(() => Effect8.succeed(null)),
|
|
1554
|
+
Effect8.withSpan("mcp.plugin.detect", {
|
|
1555
|
+
attributes: { "mcp.endpoint": url }
|
|
1556
|
+
})
|
|
1557
|
+
),
|
|
1558
|
+
// Honor upstream destructiveHint from MCP ToolAnnotations.
|
|
1559
|
+
// Bindings are fetched per scope so shadowed sources (e.g. an org-level
|
|
1560
|
+
// source overridden per-user) each resolve against their own scope's
|
|
1561
|
+
// row rather than collapsing onto whichever row the scoped adapter
|
|
1562
|
+
// sees first.
|
|
1563
|
+
resolveAnnotations: ({ ctx, sourceId, toolRows }) => Effect8.gen(function* () {
|
|
1564
|
+
const scopes = new Set(toolRows.map((row) => row.scope_id));
|
|
1565
|
+
const entries = yield* Effect8.forEach(
|
|
1566
|
+
[...scopes],
|
|
1567
|
+
(scope) => Effect8.gen(function* () {
|
|
1568
|
+
const list = yield* ctx.storage.listBindingsBySource(sourceId, scope);
|
|
1569
|
+
const byId = new Map(list.map((e) => [e.toolId, e.binding]));
|
|
1570
|
+
return [scope, byId];
|
|
1571
|
+
}),
|
|
1572
|
+
{ concurrency: "unbounded" }
|
|
1573
|
+
);
|
|
1574
|
+
const byScope = new Map(entries);
|
|
1575
|
+
const out = {};
|
|
1576
|
+
for (const row of toolRows) {
|
|
1577
|
+
const binding = byScope.get(row.scope_id)?.get(row.id);
|
|
1578
|
+
const ann = binding?.annotations;
|
|
1579
|
+
if (ann?.destructiveHint === true) {
|
|
1580
|
+
out[row.id] = {
|
|
1581
|
+
requiresApproval: true,
|
|
1582
|
+
approvalDescription: ann.title ?? binding?.toolName ?? row.id
|
|
1583
|
+
};
|
|
1584
|
+
} else {
|
|
1585
|
+
out[row.id] = { requiresApproval: false };
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return out;
|
|
1589
|
+
}),
|
|
1590
|
+
removeSource: ({ ctx, sourceId, scope }) => Effect8.gen(function* () {
|
|
1591
|
+
yield* ctx.storage.removeBindingsByNamespace(sourceId, scope);
|
|
1592
|
+
yield* ctx.storage.removeSource(sourceId, scope);
|
|
1593
|
+
}),
|
|
1594
|
+
refreshSource: () => Effect8.void,
|
|
1595
|
+
// Connection refresh for oauth2-minted sources is owned by the
|
|
1596
|
+
// canonical `"oauth2"` ConnectionProvider that core registers via
|
|
1597
|
+
// `makeOAuth2Service`. No MCP-specific provider needed.
|
|
1598
|
+
close: () => Effect8.gen(function* () {
|
|
1599
|
+
const runtime = runtimeRef.current;
|
|
1600
|
+
if (runtime) {
|
|
1601
|
+
runtime.pendingConnectors.clear();
|
|
1602
|
+
yield* ScopedCache2.invalidateAll(runtime.connectionCache);
|
|
1603
|
+
yield* Scope.close(runtime.cacheScope, Exit2.void);
|
|
1604
|
+
runtimeRef.current = null;
|
|
1605
|
+
}
|
|
1606
|
+
}).pipe(Effect8.withSpan("mcp.plugin.close")),
|
|
1607
|
+
// HTTP transport. `McpHandlers` requires `McpExtensionService`; the
|
|
1608
|
+
// host satisfies it via the `extensionService` Tag — at boot for
|
|
1609
|
+
// local, per request for cloud.
|
|
1610
|
+
routes: () => McpGroup,
|
|
1611
|
+
handlers: () => McpHandlers,
|
|
1612
|
+
extensionService: McpExtensionService
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
export {
|
|
1617
|
+
McpConnectionAuth,
|
|
1618
|
+
mcpSchema,
|
|
1619
|
+
makeMcpStore,
|
|
1620
|
+
mcpPlugin
|
|
1621
|
+
};
|
|
1622
|
+
//# sourceMappingURL=chunk-C2GNZGFJ.js.map
|