History

Revert the outbound MCP SDK v2 migration (#1697)

* Revert the outbound MCP SDK v2 migration * Drop the v2 MCP packages from the lockfile

Rhys Sullivan committed 2 months ago

Commit a5800a11 parent 54cde5e91 files · +5838 −10032Browse files

Showing the changes to packages/core/api/src/server (2 of 91 files). Show all

Unified · Split

2 files
  1. packages/core/api/src/server/executor-app.ts +14 −33
  2. packages/core/api/src/server/mcp-build.ts +31 −49
packages/core/api/src/server/executor-app.ts+14−33
@@ -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/modern18// 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 AppLayer22// 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 whose132 * 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 */
136interface McpProviderBase<RMcpAuth> {135export 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. */
144export 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. The150 * 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's550 // - `Layer.provide(mcpAuth)` satisfies the `HttpRouter.use` callback's
565 // build-time `McpAuthProvider` requirement (it registers a GET per551 // 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 the554 // 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 auth606 * Compose the MCP serving routes over the auth/sessions/reporter seams. The auth
621 * seam may require the neutral `IdentityProvider` (`RMcpAuth`); the facade provides607 * seam may require the neutral `IdentityProvider` (`RMcpAuth`); the facade provides
622 * the complete identity seam ONCE (memoized) and shares it across the build-time608 * the complete identity seam ONCE (memoized) and shares it across the build-time
623 * `Layer.provide` AND the per-request `HttpRouter.provideRequest`, so a single609 * `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 Cloudflare625 // No session store: the host serves `/mcp` transport elsewhere (the Cloudflare
640 // Agent bridge), so mount only the auth-declared discovery routes. The discovery626 // 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
packages/core/api/src/server/mcp-build.ts+31−49
@@ -1,17 +1,12 @@
1import { Effect, Layer } from "effect";1import { Effect, Layer } from "effect";
2 2
3import {3import { 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";
9import {4import {
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";
14import { buildMcpServer } from "@executor-js/host-mcp/tool-server";9import { createExecutorMcpServer } from "@executor-js/host-mcp/tool-server";
15import {10import {
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 stateless23// 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 | EngineDecorator35 DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator
35>;36>;
36 37
37type McpSessionBuildEffect = ReturnType<McpSessionBuildServer>;
38type McpModernBuildEffect = ReturnType<McpModernServerBuilder["Service"]["build"]>;
39
40/** A single build seam accepted by both the session store and modern envelope. */
41export 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; modern40 * `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 */
53export const makeMcpBuildServer = (43export 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, and53 // 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 !== undefined61 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 ...(webBaseUrl82 ...(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 from93/** 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. */