(target: Target, runDir?: string)
| 290 | }; |
| 291 | |
| 292 | export const makeMcpSurface = (target: Target, runDir?: string): McpSurface => ({ |
| 293 | url: target.mcpUrl, |
| 294 | mintBearer: (email) => Effect.promise(() => mintBearerFlow(target, email)), |
| 295 | session: (identity, options) => { |
| 296 | const mcpUrl = options?.url ?? target.mcpUrl; |
| 297 | if (runDir) installTraceparentFetch(mcpUrl, runDir); |
| 298 | // mcporter caches OAuth tokens (and the DCR client) per server NAME, so a |
| 299 | // constant name would let a later session reuse an earlier identity's token |
| 300 | // — landing in the wrong org. A unique name per session keeps each |
| 301 | // identity's OAuth isolated. The traceparent ledger keys off the URL, not |
| 302 | // this name, so it is unaffected. |
| 303 | const serverName = `${target.name}-${randomUUID().slice(0, 8)}`; |
| 304 | // Per-connection settings ride the MCP endpoint query, the ecosystem |
| 305 | // convention: `elicitation_mode` so a paused execution yields an approvalUrl |
| 306 | // instead of letting the model resume inline, and `artifacts=false` to opt |
| 307 | // the session out of the artifact surface. Both are non-defaults, so a |
| 308 | // plain session's URL carries no query at all. |
| 309 | const sessionQuery = [ |
| 310 | ...(options?.elicitationMode ? [`elicitation_mode=${options.elicitationMode}`] : []), |
| 311 | ...(options?.artifacts === false ? ["artifacts=false"] : []), |
| 312 | ].join("&"); |
| 313 | const sessionUrl = sessionQuery ? `${mcpUrl}?${sessionQuery}` : mcpUrl; |
| 314 | |
| 315 | if (target.name === "cloudflare") { |
| 316 | let clientPromise: Promise<Client> | undefined; |
| 317 | const client = () => { |
| 318 | if (!clientPromise) { |
| 319 | clientPromise = (async () => { |
| 320 | const directClient = new Client( |
| 321 | { name: serverName, version: "1.0.0" }, |
| 322 | { capabilities: {} }, |
| 323 | ); |
| 324 | const transport = new StreamableHTTPClientTransport(new URL(sessionUrl), { |
| 325 | requestInit: { headers: identity.headers ?? {} }, |
| 326 | }); |
| 327 | await directClient.connect(transport); |
| 328 | return directClient; |
| 329 | })(); |
| 330 | } |
| 331 | return clientPromise; |
| 332 | }; |
| 333 | |
| 334 | const listTools = () => |
| 335 | Effect.promise(async () => { |
| 336 | const listed = await (await client()).listTools(); |
| 337 | return listed.tools.map((tool) => tool.name); |
| 338 | }); |
| 339 | |
| 340 | const call = (name: string, args: Record<string, unknown> = {}) => |
| 341 | Effect.promise(async (): Promise<McpCallResult> => { |
| 342 | const raw = await (await client()).callTool({ name, arguments: args }); |
| 343 | const isError = Boolean((raw as { isError?: boolean })?.isError); |
| 344 | return { raw, text: textOf(raw), ok: !isError }; |
| 345 | }); |
| 346 | |
| 347 | return { |
| 348 | listTools, |
| 349 | describeTools: () => |
no test coverage detected