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