* 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 | 15 | // + that stack + plugin tuple + failure strategy) (auth + per-request executor) |
| 16 | 16 | // 3. the protected (plugin) API = makeProtectedApiLayer(plugins, { errorCapture, |
| 17 | 17 | // router: prefixed(mountPrefix) }) wrapped by (2) |
| 18 | −// 4. the MCP serving envelope = McpServingRoutes + the auth/session/modern | |
| 19 | −// builder/reporter seams (the seams) | |
| 18 | +// 4. the MCP serving envelope = McpServingRoutes + the 2-3 seams (auth/sessions | |
| 19 | +// /reporter), double-provided like the host did (the seams) | |
| 20 | 20 | // 5. the account API = makeAccountApiLayer(accountMiddleware, { router }) |
| 21 | 21 | // 6. each extensions.route (Better Auth handler, Swagger, marketing, /autumn) |
| 22 | 22 | // 7. provideMerge(boot) (+ optional requestScoped) -> the AppLayer |
| @@ -53,7 +53,6 @@ import { · 30 unmodified lines | ||
| 53 | 53 | McpErrorReporterNoop, |
| 54 | 54 | type McpAuthProvider, |
| 55 | 55 | type McpErrorReporter, |
| 56 | − type McpModernServerBuilder, | |
| 57 | 56 | type McpSessionStore, |
| 58 | 57 | } from "@executor-js/host-mcp"; |
| 59 | 58 | |
| @@ -133,32 +132,19 @@ export interface EngineProviders<REngine = never> { · 73 unmodified lines | ||
| 133 | 132 | * identity fallback sets `RMcpAuth = IdentityProvider` (self-host) and one whose |
| 134 | 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 | 136 | /** Resolve a request to an MCP `AuthOutcome` + declare the discovery routes. */ |
| 138 | 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 | 144 | /** Forward an orchestration defect to the host's capture; default no-op. */ |
| 140 | 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 | 149 | * The provider seams common to BOTH execution models (scoped + fixed): identity, |
| 164 | 150 | * the optional account API, the optional MCP envelope, and error capture. The |
| @@ -560,12 +546,12 @@ export const make = < · 395 unmodified lines | ||
| 560 | 546 | : pluginApiLive; |
| 561 | 547 | |
| 562 | 548 | // ---- (4) the MCP serving envelope (optional) -------------------------- |
| 563 | − // The serving providers, by design (mirrors makeSelfHostMcp): | |
| 549 | + // The two providers, by design (mirrors makeSelfHostMcp): | |
| 564 | 550 | // - `Layer.provide(mcpAuth)` satisfies the `HttpRouter.use` callback's |
| 565 | 551 | // build-time `McpAuthProvider` requirement (it registers a GET per |
| 566 | 552 | // provider-declared discovery path). |
| 567 | 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 | 555 | // /mcp routes carry no leftover requirements when merged into the router. |
| 570 | 556 | // The auth seam may require the neutral `IdentityProvider` (`RMcpAuth = |
| 571 | 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 | 607 | * seam may require the neutral `IdentityProvider` (`RMcpAuth`); the facade provides |
| 622 | 608 | * the complete identity seam ONCE (memoized) and shares it across the build-time |
| 623 | 609 | * `Layer.provide` AND the per-request `HttpRouter.provideRequest`, so a single |
| @@ -639,15 +625,10 @@ const buildMcpRoutes = <RMcpAuth>( · 15 unmodified lines | ||
| 639 | 625 | // No session store: the host serves `/mcp` transport elsewhere (the Cloudflare |
| 640 | 626 | // Agent bridge), so mount only the auth-declared discovery routes. The discovery |
| 641 | 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 | 629 | return McpDiscoveryRoutes.pipe(Layer.provide(mcpAuthLive)); |
| 644 | 630 | } |
| 645 | − const mcpSeams = Layer.mergeAll( | |
| 646 | − mcpAuthLive, | |
| 647 | − mcp.sessions, | |
| 648 | − mcp.modern, | |
| 649 | − mcp.reporter ?? McpErrorReporterNoop, | |
| 650 | − ); | |
| 631 | + const mcpSeams = Layer.mergeAll(mcpAuthLive, mcp.sessions, mcp.reporter ?? McpErrorReporterNoop); | |
| 651 | 632 | return McpServingRoutes.pipe(HttpRouter.provideRequest(mcpSeams), Layer.provide(mcpAuthLive)); |
| 652 | 633 | }; |
| 653 | 634 | |
| @@ -1,17 +1,12 @@ | ||
| 1 | 1 | import { Effect, Layer } from "effect"; |
| 2 | 2 | |
| 3 | −import { | |
| 4 | − McpErrorReporter, | |
| 5 | − type McpModernServerBuilder, | |
| 6 | − type McpModernServerBuildOptions, | |
| 7 | − type Principal, | |
| 8 | −} from "@executor-js/host-mcp"; | |
| 3 | +import { McpErrorReporter, type Principal } from "@executor-js/host-mcp"; | |
| 9 | 4 | import { |
| 10 | 5 | McpEngineBuildError, |
| 11 | − type McpBuildServer as McpSessionBuildServer, | |
| 12 | − type McpBuildServerOptions as McpSessionBuildOptions, | |
| 6 | + type McpBuildServer, | |
| 7 | + type McpBuildServerOptions, | |
| 13 | 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 | 10 | import { |
| 16 | 11 | artifactUrlFor, |
| 17 | 12 | type ArtifactSmokeRenderResult, |
| @@ -25,8 +20,14 @@ import { HostConfig, PluginsProvider, RequestOrgSlug } from "./scoped-executor"; · 7 unmodified lines | ||
| 25 | 20 | // --------------------------------------------------------------------------- |
| 26 | 21 | // Shared in-process MCP host helpers. |
| 27 | 22 | // |
| 28 | −// Neutral hosts build both sessionful legacy-wire connections and stateless | |
| 29 | −// modern requests from the same assembly over a scoped execution stack. | |
| 23 | +// Every host that serves MCP from one isolate (self-host, the Cloudflare QuickJS | |
| 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 | 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 | 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. | |
| 50 | − * Session callers receive the server plus its approval-owning engine; modern | |
| 51 | − * request callers receive the server directly. | |
| 39 | + * Build the per-session MCP server factory over a host's execution stack: | |
| 40 | + * `makeExecutionStack` → engine → `createExecutorMcpServer`. Hosts differ only | |
| 41 | + * in the injected stack layer (libSQL vs D1, etc.). | |
| 52 | 42 | */ |
| 53 | −export const makeMcpBuildServer = ( | |
| 54 | − executionStack: McpExecutionStackLayer, | |
| 55 | − hostOptions?: McpBuildHostOptions, | |
| 56 | −): McpBuildServer => { | |
| 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* () { | |
| 43 | +export const makeMcpBuildServer = | |
| 44 | + (executionStack: McpExecutionStackLayer, hostOptions?: McpBuildHostOptions): McpBuildServer => | |
| 45 | + (principal: Principal, options?: McpBuildServerOptions) => | |
| 46 | + Effect.gen(function* () { | |
| 68 | 47 | const { engine, executor } = yield* makeExecutionStack( |
| 69 | 48 | principal.accountId, |
| 70 | 49 | principal.organizationId, |
| 71 | 50 | principal.organizationName, |
| 72 | − { mcpResource: resource }, | |
| 51 | + { mcpResource: options?.resource }, | |
| 73 | 52 | ).pipe(Effect.withSpan("mcp.execution_stack.build")); |
| 74 | 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 | 56 | const hostConfig = yield* HostConfig; |
| 77 | 57 | return { engine, executor, webBaseUrl: hostConfig.webBaseUrl }; |
| 78 | 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 | 61 | principal.organizationSlug !== undefined |
| 80 | 62 | ? Effect.provideService(RequestOrgSlug, { slug: principal.organizationSlug }) |
| 81 | 63 | : (effect) => effect, |
| 82 | 64 | Effect.provide(executionStack), |
| 83 | 65 | Effect.mapError((cause) => new McpEngineBuildError({ cause })), |
| 84 | 66 | Effect.flatMap(({ engine, executor, webBaseUrl }) => |
| 85 | − buildMcpServer({ | |
| 67 | + createExecutorMcpServer({ | |
| 86 | 68 | engine, |
| 87 | 69 | artifacts: executor.artifacts, |
| 88 | 70 | connections: executor.connections, |
| @@ -93,20 +75,20 @@ export const makeMcpBuildServer = ( · 4 unmodified lines | ||
| 93 | 75 | ? { smokeRenderArtifact: hostOptions.smokeRenderArtifact } |
| 94 | 76 | : {}), |
| 95 | 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 | 82 | ...(webBaseUrl |
| 97 | 83 | ? { artifactUrl: artifactUrlFor(webBaseUrl, principal.organizationSlug) } |
| 98 | 84 | : {}), |
| 99 | − ...serverOptions, | |
| 85 | + ...(options ?? {}), | |
| 100 | 86 | }).pipe( |
| 101 | 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 | 93 | /** Per-host (not per-session) MCP wiring. Kept separate from |
| 112 | 94 | * `McpBuildServerOptions`, which the session store fills in per request. */ |