@executor-js/plugin-mcp 0.0.1 → 0.0.2

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