@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.
- package/README.md +19 -15
- package/dist/api/group.d.ts +113 -144
- package/dist/api/handlers.d.ts +2 -2
- package/dist/chunk-DJANY5EU.js +1325 -0
- package/dist/chunk-DJANY5EU.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 +437 -3
- package/dist/react/index.d.ts +3 -2
- package/dist/react/source-plugin.d.ts +13 -1
- package/dist/sdk/binding-store.d.ts +79 -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 +1 -0
- package/dist/sdk/per-user-auth-isolation.test.d.ts +1 -0
- package/dist/sdk/plugin.d.ts +108 -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/types.d.ts +77 -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,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
|