* Revert the outbound MCP SDK v2 migration * Drop the v2 MCP packages from the lockfile
Showing the changes to packages/core/api/src/server (2 of 91 files). Show all
Unified · Split
| @@ -15,8 +15,8 @@ · 14 unmodified lines | |||
| 15 | // + that stack + plugin tuple + failure strategy) (auth + per-request executor) | 15 | // + that stack + plugin tuple + failure strategy) (auth + per-request executor) |
| 16 | // 3. the protected (plugin) API = makeProtectedApiLayer(plugins, { errorCapture, | 16 | // 3. the protected (plugin) API = makeProtectedApiLayer(plugins, { errorCapture, |
| 17 | // router: prefixed(mountPrefix) }) wrapped by (2) | 17 | // router: prefixed(mountPrefix) }) wrapped by (2) |
| 18 | // 4. the MCP serving envelope = McpServingRoutes + the auth/session/modern | 18 | // 4. the MCP serving envelope = McpServingRoutes + the 2-3 seams (auth/sessions |
| 19 | // builder/reporter seams (the seams) | 19 | // /reporter), double-provided like the host did (the seams) |
| 20 | // 5. the account API = makeAccountApiLayer(accountMiddleware, { router }) | 20 | // 5. the account API = makeAccountApiLayer(accountMiddleware, { router }) |
| 21 | // 6. each extensions.route (Better Auth handler, Swagger, marketing, /autumn) | 21 | // 6. each extensions.route (Better Auth handler, Swagger, marketing, /autumn) |
| 22 | // 7. provideMerge(boot) (+ optional requestScoped) -> the AppLayer | 22 | // 7. provideMerge(boot) (+ optional requestScoped) -> the AppLayer |
| @@ -53,7 +53,6 @@ import { · 30 unmodified lines | |||
| 53 | McpErrorReporterNoop, | 53 | McpErrorReporterNoop, |
| 54 | type McpAuthProvider, | 54 | type McpAuthProvider, |
| 55 | type McpErrorReporter, | 55 | type McpErrorReporter, |
| 56 | type McpModernServerBuilder, | ||
| 57 | type McpSessionStore, | 56 | type McpSessionStore, |
| 58 | } from "@executor-js/host-mcp"; | 57 | } from "@executor-js/host-mcp"; |
| 59 | 58 | ||
| @@ -133,32 +132,19 @@ export interface EngineProviders<REngine = never> { · 73 unmodified lines | |||
| 133 | * identity fallback sets `RMcpAuth = IdentityProvider` (self-host) and one whose | 132 | * identity fallback sets `RMcpAuth = IdentityProvider` (self-host) and one whose |
| 134 | * MCP plane is a separate credential surface leaves it `never` (cloud). | 133 | * MCP plane is a separate credential surface leaves it `never` (cloud). |
| 135 | */ | 134 | */ |
| 136 | interface McpProviderBase<RMcpAuth> { | 135 | export interface McpProviders<RMcpAuth = never> { |
| 137 | /** Resolve a request to an MCP `AuthOutcome` + declare the discovery routes. */ | 136 | /** Resolve a request to an MCP `AuthOutcome` + declare the discovery routes. */ |
| 138 | readonly auth: Layer.Layer<McpAuthProvider, never, RMcpAuth>; | 137 | readonly auth: Layer.Layer<McpAuthProvider, never, RMcpAuth>; |
| 138 | /** | ||
| 139 | * Owns the entire serving-session lifecycle (in-process Map vs DO). Optional: | ||
| 140 | * a host that serves `/mcp` transport outside this envelope (the Cloudflare | ||
| 141 | * Agent bridge) omits it, and only the discovery routes are mounted. | ||
| 142 | */ | ||
| 143 | readonly sessions?: Layer.Layer<McpSessionStore>; | ||
| 139 | /** Forward an orchestration defect to the host's capture; default no-op. */ | 144 | /** Forward an orchestration defect to the host's capture; default no-op. */ |
| 140 | readonly reporter?: Layer.Layer<McpErrorReporter>; | 145 | readonly reporter?: Layer.Layer<McpErrorReporter>; |
| 141 | } | 146 | } |
| 142 | 147 | ||
| 143 | /** MCP providers either serve discovery alone or provide both protocol eras. */ | ||
| 144 | export type McpProviders<RMcpAuth = never> = McpProviderBase<RMcpAuth> & | ||
| 145 | ( | ||
| 146 | | { | ||
| 147 | /** | ||
| 148 | * Owns the legacy serving-session lifecycle (in-process Map vs DO). | ||
| 149 | */ | ||
| 150 | readonly sessions: Layer.Layer<McpSessionStore>; | ||
| 151 | /** Builds one stateless MCP server for each modern request. */ | ||
| 152 | readonly modern: Layer.Layer<McpModernServerBuilder>; | ||
| 153 | } | ||
| 154 | | { | ||
| 155 | /** Omitted when another platform surface serves `/mcp` transport. */ | ||
| 156 | readonly sessions?: undefined; | ||
| 157 | /** Discovery-only providers do not construct modern servers here. */ | ||
| 158 | readonly modern?: undefined; | ||
| 159 | } | ||
| 160 | ); | ||
| 161 | |||
| 162 | /** | 148 | /** |
| 163 | * The provider seams common to BOTH execution models (scoped + fixed): identity, | 149 | * The provider seams common to BOTH execution models (scoped + fixed): identity, |
| 164 | * the optional account API, the optional MCP envelope, and error capture. The | 150 | * the optional account API, the optional MCP envelope, and error capture. The |
| @@ -560,12 +546,12 @@ export const make = < · 395 unmodified lines | |||
| 560 | : pluginApiLive; | 546 | : pluginApiLive; |
| 561 | 547 | ||
| 562 | // ---- (4) the MCP serving envelope (optional) -------------------------- | 548 | // ---- (4) the MCP serving envelope (optional) -------------------------- |
| 563 | // The serving providers, by design (mirrors makeSelfHostMcp): | 549 | // The two providers, by design (mirrors makeSelfHostMcp): |
| 564 | // - `Layer.provide(mcpAuth)` satisfies the `HttpRouter.use` callback's | 550 | // - `Layer.provide(mcpAuth)` satisfies the `HttpRouter.use` callback's |
| 565 | // build-time `McpAuthProvider` requirement (it registers a GET per | 551 | // build-time `McpAuthProvider` requirement (it registers a GET per |
| 566 | // provider-declared discovery path). | 552 | // provider-declared discovery path). |
| 567 | // - `HttpRouter.provideRequest(McpSeams)` clears the route handlers' | 553 | // - `HttpRouter.provideRequest(McpSeams)` clears the route handlers' |
| 568 | // per-request `Requires` markers (auth + both eras + reporter) so the | 554 | // per-request `Requires` markers (auth + session store + reporter) so the |
| 569 | // /mcp routes carry no leftover requirements when merged into the router. | 555 | // /mcp routes carry no leftover requirements when merged into the router. |
| 570 | // The auth seam may require the neutral `IdentityProvider` (`RMcpAuth = | 556 | // The auth seam may require the neutral `IdentityProvider` (`RMcpAuth = |
| 571 | // IdentityProvider` for self-host, whose MCP auth genuinely reads the fallback; | 557 | // IdentityProvider` for self-host, whose MCP auth genuinely reads the fallback; |
| @@ -617,7 +603,7 @@ export const make = < · 45 unmodified lines | |||
| 617 | }; | 603 | }; |
| 618 | 604 | ||
| 619 | /** | 605 | /** |
| 620 | * Compose the MCP serving routes over the auth/legacy/modern/reporter seams. The auth | 606 | * Compose the MCP serving routes over the auth/sessions/reporter seams. The auth |
| 621 | * seam may require the neutral `IdentityProvider` (`RMcpAuth`); the facade provides | 607 | * seam may require the neutral `IdentityProvider` (`RMcpAuth`); the facade provides |
| 622 | * the complete identity seam ONCE (memoized) and shares it across the build-time | 608 | * the complete identity seam ONCE (memoized) and shares it across the build-time |
| 623 | * `Layer.provide` AND the per-request `HttpRouter.provideRequest`, so a single | 609 | * `Layer.provide` AND the per-request `HttpRouter.provideRequest`, so a single |
| @@ -639,15 +625,10 @@ const buildMcpRoutes = <RMcpAuth>( · 15 unmodified lines | |||
| 639 | // No session store: the host serves `/mcp` transport elsewhere (the Cloudflare | 625 | // No session store: the host serves `/mcp` transport elsewhere (the Cloudflare |
| 640 | // Agent bridge), so mount only the auth-declared discovery routes. The discovery | 626 | // Agent bridge), so mount only the auth-declared discovery routes. The discovery |
| 641 | // handlers are captured from the auth seam at build, so no per-request seams. | 627 | // handlers are captured from the auth seam at build, so no per-request seams. |
| 642 | if (mcp.sessions === undefined) { | 628 | if (!mcp.sessions) { |
| 643 | return McpDiscoveryRoutes.pipe(Layer.provide(mcpAuthLive)); | 629 | return McpDiscoveryRoutes.pipe(Layer.provide(mcpAuthLive)); |
| 644 | } | 630 | } |
| 645 | const mcpSeams = Layer.mergeAll( | 631 | const mcpSeams = Layer.mergeAll(mcpAuthLive, mcp.sessions, mcp.reporter ?? McpErrorReporterNoop); |
| 646 | mcpAuthLive, | ||
| 647 | mcp.sessions, | ||
| 648 | mcp.modern, | ||
| 649 | mcp.reporter ?? McpErrorReporterNoop, | ||
| 650 | ); | ||
| 651 | return McpServingRoutes.pipe(HttpRouter.provideRequest(mcpSeams), Layer.provide(mcpAuthLive)); | 632 | return McpServingRoutes.pipe(HttpRouter.provideRequest(mcpSeams), Layer.provide(mcpAuthLive)); |
| 652 | }; | 633 | }; |
| 653 | 634 | ||
| @@ -1,17 +1,12 @@ | |||
| 1 | import { Effect, Layer } from "effect"; | 1 | import { Effect, Layer } from "effect"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { McpErrorReporter, type Principal } from "@executor-js/host-mcp"; |
| 4 | McpErrorReporter, | ||
| 5 | type McpModernServerBuilder, | ||
| 6 | type McpModernServerBuildOptions, | ||
| 7 | type Principal, | ||
| 8 | } from "@executor-js/host-mcp"; | ||
| 9 | import { | 4 | import { |
| 10 | McpEngineBuildError, | 5 | McpEngineBuildError, |
| 11 | type McpBuildServer as McpSessionBuildServer, | 6 | type McpBuildServer, |
| 12 | type McpBuildServerOptions as McpSessionBuildOptions, | 7 | type McpBuildServerOptions, |
| 13 | } from "@executor-js/host-mcp/in-memory-session-store"; | 8 | } from "@executor-js/host-mcp/in-memory-session-store"; |
| 14 | import { buildMcpServer } from "@executor-js/host-mcp/tool-server"; | 9 | import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server"; |
| 15 | import { | 10 | import { |
| 16 | artifactUrlFor, | 11 | artifactUrlFor, |
| 17 | type ArtifactSmokeRenderResult, | 12 | type ArtifactSmokeRenderResult, |
| @@ -25,8 +20,14 @@ import { HostConfig, PluginsProvider, RequestOrgSlug } from "./scoped-executor"; · 7 unmodified lines | |||
| 25 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
| 26 | // Shared in-process MCP host helpers. | 21 | // Shared in-process MCP host helpers. |
| 27 | // | 22 | // |
| 28 | // Neutral hosts build both sessionful legacy-wire connections and stateless | 23 | // Every host that serves MCP from one isolate (self-host, the Cloudflare QuickJS |
| 29 | // modern requests from the same assembly over a scoped execution stack. | 24 | // host) builds its per-session McpServer the same way — assemble the scoped |
| 25 | // engine via `makeExecutionStack`, wrap it with `createExecutorMcpServer` — and | ||
| 26 | // reports orchestration defects through the same console `ErrorCapture` seam. | ||
| 27 | // These two factories are the single home for that logic; a host supplies ONLY | ||
| 28 | // its fully-provided execution-stack layer and its `ErrorCapture` layer. The | ||
| 29 | // cross-isolate variant (cloud's Durable Object store) is the exception that | ||
| 30 | // builds its engine inside the DO. | ||
| 30 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
| 31 | 32 | ||
| 32 | /** The five execution-stack seams a host fully provides (no residual). */ | 33 | /** The five execution-stack seams a host fully provides (no residual). */ |
| @@ -34,55 +35,36 @@ export type McpExecutionStackLayer = Layer.Layer< · 1 unmodified line | |||
| 34 | DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator | 35 | DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator |
| 35 | >; | 36 | >; |
| 36 | 37 | ||
| 37 | type McpSessionBuildEffect = ReturnType<McpSessionBuildServer>; | ||
| 38 | type McpModernBuildEffect = ReturnType<McpModernServerBuilder["Service"]["build"]>; | ||
| 39 | |||
| 40 | /** A single build seam accepted by both the session store and modern envelope. */ | ||
| 41 | export interface McpBuildServer { | ||
| 42 | /** Build a connection-lifetime server and retain its engine for session-owned approvals. */ | ||
| 43 | (principal: Principal, options: McpSessionBuildOptions): McpSessionBuildEffect; | ||
| 44 | /** Build a stateless server for one modern request. */ | ||
| 45 | (principal: Principal, options: McpModernServerBuildOptions): McpModernBuildEffect; | ||
| 46 | } | ||
| 47 | |||
| 48 | /** | 38 | /** |
| 49 | * Build the unified MCP server factory over a host's execution stack. | 39 | * Build the per-session MCP server factory over a host's execution stack: |
| 50 | * Session callers receive the server plus its approval-owning engine; modern | 40 | * `makeExecutionStack` → engine → `createExecutorMcpServer`. Hosts differ only |
| 51 | * request callers receive the server directly. | 41 | * in the injected stack layer (libSQL vs D1, etc.). |
| 52 | */ | 42 | */ |
| 53 | export const makeMcpBuildServer = ( | 43 | export const makeMcpBuildServer = |
| 54 | executionStack: McpExecutionStackLayer, | 44 | (executionStack: McpExecutionStackLayer, hostOptions?: McpBuildHostOptions): McpBuildServer => |
| 55 | hostOptions?: McpBuildHostOptions, | 45 | (principal: Principal, options?: McpBuildServerOptions) => |
| 56 | ): McpBuildServer => { | 46 | Effect.gen(function* () { |
| 57 | function build(principal: Principal, options: McpSessionBuildOptions): McpSessionBuildEffect; | ||
| 58 | function build(principal: Principal, options: McpModernServerBuildOptions): McpModernBuildEffect; | ||
| 59 | function build( | ||
| 60 | principal: Principal, | ||
| 61 | options: McpSessionBuildOptions | McpModernServerBuildOptions, | ||
| 62 | ): Effect.Effect< | ||
| 63 | Effect.Success<McpSessionBuildEffect> | Effect.Success<McpModernBuildEffect>, | ||
| 64 | Effect.Error<McpSessionBuildEffect> | Effect.Error<McpModernBuildEffect> | ||
| 65 | > { | ||
| 66 | const { resource, ...serverOptions } = options; | ||
| 67 | return Effect.gen(function* () { | ||
| 68 | const { engine, executor } = yield* makeExecutionStack( | 47 | const { engine, executor } = yield* makeExecutionStack( |
| 69 | principal.accountId, | 48 | principal.accountId, |
| 70 | principal.organizationId, | 49 | principal.organizationId, |
| 71 | principal.organizationName, | 50 | principal.organizationName, |
| 72 | { mcpResource: resource }, | 51 | { mcpResource: options?.resource }, |
| 73 | ).pipe(Effect.withSpan("mcp.execution_stack.build")); | 52 | ).pipe(Effect.withSpan("mcp.execution_stack.build")); |
| 74 | // Read inside the provided boundary: `webBaseUrl` is a host seam, and | 53 | // Read inside the provided boundary: `webBaseUrl` is a host seam, and |
| 75 | // hosts that cannot know their public URL at boot leave it unset. | 54 | // hosts that can't know their public URL at boot leave it unset — in |
| 55 | // which case artifacts still persist but carry no deep link. | ||
| 76 | const hostConfig = yield* HostConfig; | 56 | const hostConfig = yield* HostConfig; |
| 77 | return { engine, executor, webBaseUrl: hostConfig.webBaseUrl }; | 57 | return { engine, executor, webBaseUrl: hostConfig.webBaseUrl }; |
| 78 | }).pipe( | 58 | }).pipe( |
| 59 | // Pin browser-handoff URLs to the principal's org slug when present; | ||
| 60 | // absent slug leaves the service unprovided and the URL stays bare. | ||
| 79 | principal.organizationSlug !== undefined | 61 | principal.organizationSlug !== undefined |
| 80 | ? Effect.provideService(RequestOrgSlug, { slug: principal.organizationSlug }) | 62 | ? Effect.provideService(RequestOrgSlug, { slug: principal.organizationSlug }) |
| 81 | : (effect) => effect, | 63 | : (effect) => effect, |
| 82 | Effect.provide(executionStack), | 64 | Effect.provide(executionStack), |
| 83 | Effect.mapError((cause) => new McpEngineBuildError({ cause })), | 65 | Effect.mapError((cause) => new McpEngineBuildError({ cause })), |
| 84 | Effect.flatMap(({ engine, executor, webBaseUrl }) => | 66 | Effect.flatMap(({ engine, executor, webBaseUrl }) => |
| 85 | buildMcpServer({ | 67 | createExecutorMcpServer({ |
| 86 | engine, | 68 | engine, |
| 87 | artifacts: executor.artifacts, | 69 | artifacts: executor.artifacts, |
| 88 | connections: executor.connections, | 70 | connections: executor.connections, |
| @@ -93,20 +75,20 @@ export const makeMcpBuildServer = ( · 4 unmodified lines | |||
| 93 | ? { smokeRenderArtifact: hostOptions.smokeRenderArtifact } | 75 | ? { smokeRenderArtifact: hostOptions.smokeRenderArtifact } |
| 94 | : {}), | 76 | : {}), |
| 95 | ...(hostOptions?.onArtifactUsage ? { onArtifactUsage: hostOptions.onArtifactUsage } : {}), | 77 | ...(hostOptions?.onArtifactUsage ? { onArtifactUsage: hostOptions.onArtifactUsage } : {}), |
| 78 | // Same org pinning as `RequestOrgSlug` above: self-host serves its | ||
| 79 | // console under `/<org-slug>` (`default` when unconfigured), so the | ||
| 80 | // deep link carries the principal's slug rather than relying on the | ||
| 81 | // browser's active org to canonicalize a bare path after landing. | ||
| 96 | ...(webBaseUrl | 82 | ...(webBaseUrl |
| 97 | ? { artifactUrl: artifactUrlFor(webBaseUrl, principal.organizationSlug) } | 83 | ? { artifactUrl: artifactUrlFor(webBaseUrl, principal.organizationSlug) } |
| 98 | : {}), | 84 | : {}), |
| 99 | ...serverOptions, | 85 | ...(options ?? {}), |
| 100 | }).pipe( | 86 | }).pipe( |
| 101 | Effect.withSpan("mcp.server.create"), | 87 | Effect.withSpan("mcp.server.create"), |
| 102 | Effect.map((mcpServer) => ("sessionful" in options ? { mcpServer, engine } : mcpServer)), | 88 | Effect.map((mcpServer) => ({ mcpServer, engine })), |
| 103 | ), | 89 | ), |
| 104 | ), | 90 | ), |
| 105 | ); | 91 | ); |
| 106 | } | ||
| 107 | |||
| 108 | return build; | ||
| 109 | }; | ||
| 110 | 92 | ||
| 111 | /** Per-host (not per-session) MCP wiring. Kept separate from | 93 | /** Per-host (not per-session) MCP wiring. Kept separate from |
| 112 | * `McpBuildServerOptions`, which the session store fills in per request. */ | 94 | * `McpBuildServerOptions`, which the session store fills in per request. */ |