History

feat: wire PM execute through runtime adapters and align v2 test/tooling

Rhys Sullivan committed 8 months ago

Commit aa5b69e1 parent f6167a735 files · +829 −258Browse files

Unified · Split

35 files
  1. v2/PLAN.md +25 −10
  2. v2/apps/cli/src/main.ts +1 −0
  3. v2/apps/convex/src/main.ts +1 −0
  4. v2/apps/pm/README.md +2 −1
  5. v2/apps/pm/package.json +3 −1
  6. v2/apps/pm/src/main.ts +67 −1
  7. v2/apps/web/src/main.ts +1 −0
  8. v2/bun.lock +8 −1
  9. v2/knip.json +3 −0
  10. v2/package.json +2 −1
  11. v2/packages/ai-sdk-adapter/src/index.ts +1 −0
  12. v2/packages/approvals/src/index.ts +1 −0
  13. v2/packages/confect/package.json +0 −1
  14. v2/packages/confect/src/rpc/server.ts +0 −1
  15. v2/packages/confect/src/rpc/telemetry.ts +1 −1
  16. v2/packages/domain/src/index.ts +1 −0
  17. v2/packages/engine/README.md +1 −0
  18. v2/packages/engine/package.json +4 −2
  19. v2/packages/engine/src/deno-subprocess-runner.test.ts +38 −38
  20. v2/packages/engine/src/index.ts +16 −0
  21. v2/packages/engine/src/openapi-execution.test.ts +98 −46
  22. v2/packages/engine/src/runtime-adapters.test.ts +96 −0
  23. v2/packages/engine/src/runtime-adapters.ts +212 −0
  24. v2/packages/engine/src/tool-providers.test.ts +83 −86
  25. v2/packages/mcp-gateway/README.md +1 −1
  26. v2/packages/mcp-gateway/src/index.ts +3 −0
  27. v2/packages/mcp-gateway/src/server.ts +83 −0
  28. v2/packages/oauth/src/index.ts +1 −0
  29. v2/packages/rpc/src/index.ts +1 −0
  30. v2/packages/sdk/src/index.ts +1 −0
  31. v2/packages/secrets/src/index.ts +1 −0
  32. v2/packages/source-manager/package.json +5 −0
  33. v2/packages/source-manager/src/openapi-extraction.test.ts +55 −55
  34. v2/packages/source-manager/src/openapi-extraction.ts +12 −12
  35. v2/packages/sync/src/index.ts +1 −0
v2/PLAN.md+25−10
@@ -47,6 +47,12 @@ v2 changes the default: · 46 unmodified lines
47- `cloud` and `self-hosted` are not separate architecture branches.47- `cloud` and `self-hosted` are not separate architecture branches.
48- They are both **remote Convex targets**; only URL/auth/deployment ownership differs.48- They are both **remote Convex targets**; only URL/auth/deployment ownership differs.
49 49
50### Runtime targets (adapter-based)
51
52- **`deno-subprocess`**: local sandbox runtime (current local secure target in v2).
53- **`cloudflare-worker-loader`**: production cloud runtime target.
54- **`node:vm`**: explicit unsafe/dev fallback only, not default.
55
50---56---
51 57
52## 4) Control + execution model58## 4) Control + execution model
@@ -391,27 +397,36 @@ Implemented in `v2` so far: · 338 unmodified lines
391- monorepo app/package skeleton397- monorepo app/package skeleton
392- `confect` imported from quickhub398- `confect` imported from quickhub
393- schema package scaffold with Effect models and event envelope399- schema package scaffold with Effect models and event envelope
400- schema-driven runtime tool models (canonical descriptor + OpenAPI invocation/manifest)
394- domain ID groundwork in `schema/src/ids.ts`401- domain ID groundwork in `schema/src/ids.ts`
395- Convex table ID groundwork in `persistence-convex/src/convex-ids.ts`402- Convex table ID groundwork in `persistence-convex/src/convex-ids.ts`
396- shared persistence ports for `SourceStore` and `ToolArtifactStore`403- shared persistence ports for `SourceStore` and `ToolArtifactStore`
397- local persistence adapters for source/tool artifacts404- local persistence adapters for source/tool artifacts
398- local-only snapshot/WAL contract and persistence implementation405- local-only snapshot/WAL contract and persistence implementation
399- OpenAPI extraction + artifact refresh/reuse flow406- OpenAPI extraction + artifact refresh/reuse flow
400- service-first wiring (`Context.Tag` + Layer) in source manager and local persistence407- provider registry contracts and routing in `engine`
408- OpenAPI provider invocation + in-memory provider path through registry
409- local in-process JS runner with `tools.*` proxy dispatch
410- Deno subprocess runtime path with IPC tool-call proxying and tests
411- runtime adapter contract + registry scaffold in `engine` with mapped adapters (`local-inproc`, `deno-subprocess`, `cloudflare-worker-loader`)
412- minimal PM execute lifecycle wired through MCP `executor.execute` -> runtime adapter registry (single run, no persisted run state yet)
413- vertical test: OpenAPI spec -> manifest -> execute code -> HTTP tool call
414- service-first wiring (`Context.Tag` + Layer) across source manager and persistence/local runtime orchestration
401 415
402Not implemented yet:416Not implemented yet:
403- provider registry and canonical tool descriptor routing in engine/runtime417- `tools.executor.sources.add/list/remove` end-to-end PM + MCP gateway wiring
404- execute lifecycle through PM beyond stub MCP tool418- approval adapter/state machine integration (pending/resume/deny persistence) in v2 runtime path
405- MCP/GraphQL providers and in-memory tool registration/invocation path419- provider/runtime conformance suite beyond current baseline tests (timeouts, cancellation, pending approval)
420- Cloudflare worker loader adapter execution integration (current adapter is explicit scaffold/not-implemented)
421- MCP/GraphQL provider implementations
406- full rpc/sdk adapter wiring and sync engine422- full rpc/sdk adapter wiring and sync engine
407 423
408---424---
409 425
410## 19) Immediate next steps426## 19) Immediate next steps
411 427
4121. Define canonical provider tool descriptor + provider service contracts (`discover` / `invoke`).4281. Add first `tools.executor.sources.add/list/remove` control operations end-to-end in PM + MCP gateway.
4132. Implement engine-side provider registry and routing for tool invocation.4292. Implement approval adapter contract with persisted pending/resume/deny flow for runtime tool calls.
4143. Wire minimal `apps/pm` execute flow (single run lifecycle) using provider routing.4303. Add provider/runtime conformance tests (invoke success/failure, timeout, pending approval, cancel).
4154. Implement first end-to-end providers: `openapi` (network) and `in_memory` (AI-SDK style).4314. Add Cloudflare worker loader adapter integration against the runtime adapter contract.
4165. Add first `tools.executor.sources.add/list/remove` control operations in PM gateway.4325. Extend PM execute lifecycle from single-run scaffold to persisted run state + streaming status.
4176. Add provider conformance tests and one vertical test: OpenAPI spec -> manifest -> execute tool call.
v2/apps/cli/src/main.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/apps/convex/src/main.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/apps/pm/README.md+2−1
@@ -5,4 +5,5 @@ Local Process Manager app scaffold for Executor v2. · 4 unmodified lines
5Current scaffold includes:5Current scaffold includes:
6- local MCP endpoint at `POST /mcp`6- local MCP endpoint at `POST /mcp`
7- health endpoint at `GET /healthz`7- health endpoint at `GET /healthz`
8- MCP stub tool routing via `@executor-v2/mcp-gateway`8- MCP tool routing via `@executor-v2/mcp-gateway`
9- `executor.execute` wired to engine runtime adapters (`local-inproc` default, `deno-subprocess` optional)
v2/apps/pm/package.json+3−1
@@ -8,6 +8,8 @@ · 7 unmodified lines
8 "typecheck": "bunx tsc --noEmit -p tsconfig.json"8 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
9 },9 },
10 "dependencies": {10 "dependencies": {
11 "@executor-v2/mcp-gateway": "workspace:*"11 "@executor-v2/engine": "workspace:*",
12 "@executor-v2/mcp-gateway": "workspace:*",
13 "effect": "^3.19.8"
12 }14 }
13}15}
v2/apps/pm/src/main.ts+67−1
@@ -1,12 +1,78 @@
1import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway";1import {
2 makeCloudflareWorkerLoaderRuntimeAdapter,
3 makeDenoSubprocessRuntimeAdapter,
4 makeLocalInProcessRuntimeAdapter,
5 makeRuntimeAdapterRegistry,
6 makeToolProviderRegistry,
7 ToolProviderRegistryService,
8} from "@executor-v2/engine";
9import {
10 handleMcpHttpRequest,
11 type ExecuteToolInput,
12 type ExecuteToolResult,
13} from "@executor-v2/mcp-gateway";
14import * as Effect from "effect/Effect";
15import * as Either from "effect/Either";
2 16
3const port = Number(Bun.env.PORT ?? 8787);17const port = Number(Bun.env.PORT ?? 8787);
4 18
19const runtimeRegistry = makeRuntimeAdapterRegistry([
20 makeLocalInProcessRuntimeAdapter(),
21 makeDenoSubprocessRuntimeAdapter(),
22 makeCloudflareWorkerLoaderRuntimeAdapter(),
23]);
24
25const toolRegistry = makeToolProviderRegistry([]);
26
27const errorToText = (error: unknown): string => {
28 if (error instanceof Error) {
29 return error.message;
30 }
31
32 if (
33 typeof error === "object" &&
34 error !== null &&
35 "message" in error &&
36 typeof error.message === "string"
37 ) {
38 return error.message;
39 }
40
41 return String(error);
42};
43
44const executeTool = async (input: ExecuteToolInput): Promise<ExecuteToolResult> => {
45 const result = await Effect.runPromise(
46 Effect.either(
47 runtimeRegistry
48 .execute({
49 runtimeKind: input.runtimeKind ?? "local-inproc",
50 code: input.code,
51 tools: [],
52 })
53 .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)),
54 ),
55 );
56
57 if (Either.isLeft(result)) {
58 return {
59 isError: true,
60 error: errorToText(result.left),
61 };
62 }
63
64 return {
65 isError: false,
66 output: result.right,
67 };
68};
69
5const handleMcp = async (request: Request): Promise<Response> =>70const handleMcp = async (request: Request): Promise<Response> =>
6 handleMcpHttpRequest(request, {71 handleMcpHttpRequest(request, {
7 target: "local",72 target: "local",
8 serverName: "executor-v2-pm",73 serverName: "executor-v2-pm",
9 serverVersion: "0.0.0",74 serverVersion: "0.0.0",
75 execute: executeTool,
10 });76 });
11 77
12const server = Bun.serve({78const server = Bun.serve({
v2/apps/web/src/main.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/bun.lock+8−1
@@ -25,7 +25,9 @@ · 24 unmodified lines
25 "apps/pm": {25 "apps/pm": {
26 "name": "@executor-v2/app-pm",26 "name": "@executor-v2/app-pm",
27 "dependencies": {27 "dependencies": {
28 "@executor-v2/engine": "workspace:*",
28 "@executor-v2/mcp-gateway": "workspace:*",29 "@executor-v2/mcp-gateway": "workspace:*",
30 "effect": "^3.19.8",
29 },31 },
30 },32 },
31 "apps/web": {33 "apps/web": {
@@ -50,7 +52,6 @@ · 18 unmodified lines
50 "next": "^16.1.6",52 "next": "^16.1.6",
51 },53 },
52 "devDependencies": {54 "devDependencies": {
53 "@effect/vitest": "^0.27.0",
54 "@types/node": "^20.19.9",55 "@types/node": "^20.19.9",
55 "typescript": "^5.9.2",56 "typescript": "^5.9.2",
56 "vitest": "^3.2.4",57 "vitest": "^3.2.4",
@@ -67,7 +68,9 @@ · 10 unmodified lines
67 "effect": "^3.19.8",68 "effect": "^3.19.8",
68 },69 },
69 "devDependencies": {70 "devDependencies": {
71 "@effect/vitest": "^0.27.0",
70 "@executor-v2/source-manager": "workspace:*",72 "@executor-v2/source-manager": "workspace:*",
73 "vitest": "^3.2.4",
71 },74 },
72 },75 },
73 "packages/mcp-gateway": {76 "packages/mcp-gateway": {
@@ -102,6 +105,10 @@ · 28 unmodified lines
102 "@executor-v2/schema": "workspace:*",105 "@executor-v2/schema": "workspace:*",
103 "effect": "^3.19.8",106 "effect": "^3.19.8",
104 },107 },
108 "devDependencies": {
109 "@effect/vitest": "^0.27.0",
110 "vitest": "^3.2.4",
111 },
105 },112 },
106 "packages/state-contracts": {113 "packages/state-contracts": {
107 "name": "@executor-v2/persistence-ports",114 "name": "@executor-v2/persistence-ports",
v2/knip.json+3−0
@@ -0,0 +1,3 @@
1{
2 "$schema": "https://unpkg.com/knip@latest/schema.json"
3}
v2/package.json+2−1
@@ -9,7 +9,8 @@ · 8 unmodified lines
9 ],9 ],
10 "scripts": {10 "scripts": {
11 "prepare": "effect-language-service patch",11 "prepare": "effect-language-service patch",
12 "typecheck": "tsc --noEmit"12 "typecheck": "tsc --noEmit",
13 "knip": "bunx knip --config knip.json"
13 },14 },
14 "devDependencies": {15 "devDependencies": {
15 "@effect/language-service": "^0.76.0",16 "@effect/language-service": "^0.76.0",
v2/packages/ai-sdk-adapter/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/approvals/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/confect/package.json+0−1
@@ -27,7 +27,6 @@ · 26 unmodified lines
27 "next": "^16.1.6"27 "next": "^16.1.6"
28 },28 },
29 "devDependencies": {29 "devDependencies": {
30 "@effect/vitest": "^0.27.0",
31 "@types/node": "^20.19.9",30 "@types/node": "^20.19.9",
32 "typescript": "^5.9.2",31 "typescript": "^5.9.2",
33 "vitest": "^3.2.4"32 "vitest": "^3.2.4"
v2/packages/confect/src/rpc/server.ts+0−1
@@ -44,7 +44,6 @@ export { Rpc, RpcGroup, RpcMiddleware }; · 43 unmodified lines
44 44
45export {45export {
46 WrapperTypeId,46 WrapperTypeId,
47 type WrapperTypeId as WrapperTypeIdType,
48 type Wrapper,47 type Wrapper,
49 isWrapper,48 isWrapper,
50 wrap,49 wrap,
v2/packages/confect/src/rpc/telemetry.ts+1−1
@@ -9,7 +9,7 @@ export const convexRequestIdHeader = "convex-request-id"; · 8 unmodified lines
9 9
10export type RpcClientKind = "query" | "mutation" | "action";10export type RpcClientKind = "query" | "mutation" | "action";
11 11
12export interface RpcTelemetryContext {12interface RpcTelemetryContext {
13 readonly traceparent: string;13 readonly traceparent: string;
14 readonly b3?: string;14 readonly b3?: string;
15 readonly tracestate?: string;15 readonly tracestate?: string;
v2/packages/domain/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/engine/README.md+1−0
@@ -8,4 +8,5 @@ Current scaffold includes: · 7 unmodified lines
8- OpenAPI provider invocation and manifest-to-descriptor conversion helpers8- OpenAPI provider invocation and manifest-to-descriptor conversion helpers
9- minimal in-process JavaScript runner with `tools.*` proxy dispatch into provider registry9- minimal in-process JavaScript runner with `tools.*` proxy dispatch into provider registry
10- Deno subprocess runner with line-delimited JSON IPC for proxied `tools.*` calls10- Deno subprocess runner with line-delimited JSON IPC for proxied `tools.*` calls
11- runtime adapter contract + registry (`local-inproc`, `deno-subprocess`, `cloudflare-worker-loader` scaffold)
11- vertical integration test covering OpenAPI extraction -> descriptor conversion -> code execution -> HTTP call12- vertical integration test covering OpenAPI extraction -> descriptor conversion -> code execution -> HTTP call
v2/packages/engine/package.json+4−2
@@ -6,7 +6,7 @@ · 5 unmodified lines
6 ".": "./src/index.ts"6 ".": "./src/index.ts"
7 },7 },
8 "scripts": {8 "scripts": {
9 "test": "bun test src/*.test.ts",9 "test": "bunx vitest run --no-watch src/*.test.ts",
10 "typecheck": "bunx tsc --noEmit -p tsconfig.json"10 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
11 },11 },
12 "dependencies": {12 "dependencies": {
@@ -15,6 +15,8 @@ · 2 unmodified lines
15 "effect": "^3.19.8"15 "effect": "^3.19.8"
16 },16 },
17 "devDependencies": {17 "devDependencies": {
18 "@executor-v2/source-manager": "workspace:*"18 "@effect/vitest": "^0.27.0",
19 "@executor-v2/source-manager": "workspace:*",
20 "vitest": "^3.2.4"
19 }21 }
20}22}
v2/packages/engine/src/deno-subprocess-runner.test.ts+38−38
@@ -1,9 +1,10 @@
1import { describe, expect, it } from "@effect/vitest";
1import {2import {
2 CanonicalToolDescriptorSchema,3 CanonicalToolDescriptorSchema,
3 type CanonicalToolDescriptor,4 type CanonicalToolDescriptor,
4} from "@executor-v2/schema";5} from "@executor-v2/schema";
5import { describe, expect, test } from "bun:test";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Either from "effect/Either";
7import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
8 9
9import {10import {
@@ -33,30 +34,30 @@ const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor · 23 unmodified lines
33});34});
34 35
35describe("executeJavaScriptInDenoSubprocess", () => {36describe("executeJavaScriptInDenoSubprocess", () => {
36 test("runs code in Deno subprocess and proxies tool calls", async () => {37 it.effect("runs code in Deno subprocess and proxies tool calls", () =>
37 const sumProvider: ToolProvider = {38 Effect.gen(function* () {
38 kind: "in_memory",39 const sumProvider: ToolProvider = {
39 invoke: (input) =>40 kind: "in_memory",
40 Effect.gen(function* () {41 invoke: (input) =>
41 if (input.tool.toolId !== "sum") {42 Effect.gen(function* () {
43 if (input.tool.toolId !== "sum") {
44 return {
45 output: `unknown tool: ${input.tool.toolId}`,
46 isError: true,
47 } as const;
48 }
49
50 const args = input.args as { a: number; b: number };
42 return {51 return {
43 output: `unknown tool: ${input.tool.toolId}`,52 output: args.a + args.b,
44 isError: true,53 isError: false,
45 } as const;54 } as const;
46 }55 }),
47 56 };
48 const args = input.args as { a: number; b: number };
49 return {
50 output: args.a + args.b,
51 isError: false,
52 } as const;
53 }),
54 };
55 57
56 const registry = makeToolProviderRegistry([sumProvider]);58 const registry = makeToolProviderRegistry([sumProvider]);
57 59
58 const result = await Effect.runPromise(60 const result = yield* executeJavaScriptInDenoSubprocess({
59 executeJavaScriptInDenoSubprocess({
60 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",61 code: "console.log('hello'); return await tools.sum({ a: 2, b: 3 });",
61 tools: [62 tools: [
62 {63 {
@@ -65,31 +66,30 @@ describe("executeJavaScriptInDenoSubprocess", () => { · 2 unmodified lines
65 },66 },
66 ],67 ],
67 timeoutMs: 10_000,68 timeoutMs: 10_000,
68 }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),69 }).pipe(Effect.provideService(ToolProviderRegistryService, registry));
69 );
70 70
71 expect(result).toBe(5);71 expect(result).toBe(5);
72 });72 }),
73 );
73 74
74 test("returns a typed error when Deno executable is missing", async () => {75 it.effect("returns a typed error when Deno executable is missing", () =>
75 const registry = makeToolProviderRegistry([]);76 Effect.gen(function* () {
77 const registry = makeToolProviderRegistry([]);
76 78
77 const result = await Effect.runPromise(79 const result = yield* Effect.either(
78 Effect.either(
79 executeJavaScriptInDenoSubprocess({80 executeJavaScriptInDenoSubprocess({
80 code: "return 1;",81 code: "return 1;",
81 tools: [],82 tools: [],
82 denoExecutable: "/definitely-missing-deno-binary",83 denoExecutable: "/definitely-missing-deno-binary",
83 timeoutMs: 1_000,84 timeoutMs: 1_000,
84 }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),85 }).pipe(Effect.provideService(ToolProviderRegistryService, registry)),
85 ),86 );
86 );
87
88 if (result._tag === "Right") {
89 throw new Error("expected missing Deno executable to fail");
90 }
91 87
92 expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError);88 expect(Either.isLeft(result)).toBe(true);
93 expect(result.left.operation).toBe("spawn");89 if (Either.isLeft(result)) {
94 });90 expect(result.left).toBeInstanceOf(DenoSubprocessRunnerError);
91 expect(result.left.operation).toBe("spawn");
92 }
93 }),
94 );
95});95});
v2/packages/engine/src/index.ts+16−0
@@ -11,6 +11,22 @@ export { · 10 unmodified lines
11 type ExecuteJavaScriptInput,11 type ExecuteJavaScriptInput,
12 type RunnableTool,12 type RunnableTool,
13} from "./local-runner";13} from "./local-runner";
14export {
15 RuntimeAdapterRegistryLive,
16 RuntimeAdapterRegistryError,
17 RuntimeAdapterRegistryService,
18 RuntimeAdapterError,
19 makeCloudflareWorkerLoaderRuntimeAdapter,
20 makeDenoSubprocessRuntimeAdapter,
21 makeLocalInProcessRuntimeAdapter,
22 makeRuntimeAdapterRegistry,
23 type RuntimeAdapter,
24 type RuntimeAdapterKind,
25 type RuntimeAdapterRegistry,
26 type RuntimeExecuteError,
27 type RuntimeExecuteInput,
28 type RuntimeRunnableTool,
29} from "./runtime-adapters";
14export {30export {
15 makeOpenApiToolProvider,31 makeOpenApiToolProvider,
16 openApiToolDescriptorsFromManifest,32 openApiToolDescriptorsFromManifest,
v2/packages/engine/src/openapi-execution.test.ts+98−46
@@ -1,9 +1,10 @@
1import {1import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2 type ToolArtifactStore,2
3} from "@executor-v2/persistence-ports";3import { describe, expect, it } from "@effect/vitest";
4import { type ToolArtifactStore } from "@executor-v2/persistence-ports";
4import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";5import { type Source, SourceSchema, type ToolArtifact } from "@executor-v2/schema";
5import { makeSourceManagerService } from "@executor-v2/source-manager";6import { makeSourceManagerService } from "@executor-v2/source-manager";
6import { describe, expect, test } from "bun:test";7import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
8import * as Option from "effect/Option";9import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
@@ -27,51 +28,104 @@ type TestServer = { · 17 unmodified lines
27 query: string;28 query: string;
28 apiKey: string | null;29 apiKey: string | null;
29 }>;30 }>;
30 server: Bun.Server<unknown>;31 close: () => Promise<void>;
31};32};
32 33
33const makeTestServer = Effect.acquireRelease(34class TestServerReleaseError extends Data.TaggedError("TestServerReleaseError")<{
34 Effect.sync<TestServer>(() => {35 message: string;
35 const requests: TestServer["requests"] = [];36}> {}
36 37
37 const server = Bun.serve({38const jsonResponse = (res: ServerResponse, statusCode: number, body: unknown): void => {
38 port: 0,39 res.statusCode = statusCode;
39 fetch: async (request) => {40 res.setHeader("content-type", "application/json");
40 const url = new URL(request.url);41 res.end(JSON.stringify(body));
41 42};
42 if (url.pathname === "/users/u123") {43
43 requests.push({44const getHeaderValue = (
44 path: url.pathname,45 req: IncomingMessage,
45 query: url.search,46 key: string,
46 apiKey: request.headers.get("x-api-key"),47): string | null => {
47 });48 const value = req.headers[key];
49 if (typeof value === "string") {
50 return value;
51 }
48 52
49 return Response.json({53 if (Array.isArray(value)) {
50 id: "u123",54 return value[0] ?? null;
51 verbose: url.searchParams.get("verbose") === "true",55 }
52 apiKey: request.headers.get("x-api-key"),56
57 return null;
58};
59
60const makeTestServer = Effect.acquireRelease(
61 Effect.promise<TestServer>(
62 () =>
63 new Promise<TestServer>((resolve, reject) => {
64 const requests: TestServer["requests"] = [];
65
66 const server = createServer((req, res) => {
67 const host = getHeaderValue(req, "host") ?? "127.0.0.1";
68 const url = new URL(req.url ?? "/", `http://${host}`);
69
70 if (url.pathname === "/users/u123") {
71 requests.push({
72 path: url.pathname,
73 query: url.search,
74 apiKey: getHeaderValue(req, "x-api-key"),
75 });
76
77 jsonResponse(res, 200, {
78 id: "u123",
79 verbose: url.searchParams.get("verbose") === "true",
80 apiKey: getHeaderValue(req, "x-api-key"),
81 });
82 return;
83 }
84
85 jsonResponse(res, 404, { error: "not found" });
86 });
87
88 server.once("error", (error) => {
89 reject(error);
90 });
91
92 server.listen(0, "127.0.0.1", () => {
93 const address = server.address();
94 if (!address || typeof address === "string") {
95 reject(new Error("failed to resolve test server address"));
96 return;
97 }
98
99 resolve({
100 baseUrl: `http://127.0.0.1:${address.port}`,
101 requests,
102 close: () =>
103 new Promise<void>((closeResolve, closeReject) => {
104 server.close((error) => {
105 if (error) {
106 closeReject(error);
107 return;
108 }
109 closeResolve();
110 });
111 }),
53 });112 });
54 }113 });
55 114 })
56 return Response.json({ error: "not found" }, { status: 404 });115 ),
57 },116 (testServer) =>
58 });117 Effect.tryPromise({
59 118 try: () => testServer.close(),
60 return {119 catch: (cause) =>
61 baseUrl: `http://127.0.0.1:${server.port}`,120 new TestServerReleaseError({
62 requests,121 message: cause instanceof Error ? cause.message : String(cause),
63 server,122 }),
64 };123 }).pipe(Effect.orDie),
65 }),
66 ({ server }) =>
67 Effect.sync(() => {
68 server.stop(true);
69 }),
70);124);
71 125
72describe("OpenAPI execution vertical slice", () => {126describe("OpenAPI execution vertical slice", () => {
73 test("extracts OpenAPI tools and executes code against provider", async () => {127 it.scoped("extracts OpenAPI tools and executes code against provider", () =>
74 const program = Effect.gen(function* () {128 Effect.gen(function* () {
75 const testServer = yield* makeTestServer;129 const testServer = yield* makeTestServer;
76 130
77 const openApiSpec = {131 const openApiSpec = {
@@ -185,8 +239,6 @@ return await tools.getUser({ · 107 unmodified lines
185 expect(testServer.requests[0]?.path).toBe("/users/u123");239 expect(testServer.requests[0]?.path).toBe("/users/u123");
186 expect(testServer.requests[0]?.query).toBe("?verbose=true");240 expect(testServer.requests[0]?.query).toBe("?verbose=true");
187 expect(testServer.requests[0]?.apiKey).toBe("sk_test");241 expect(testServer.requests[0]?.apiKey).toBe("sk_test");
188 });242 }),
189 243 );
190 await Effect.runPromise(program.pipe(Effect.scoped));
191 });
192});244});
v2/packages/engine/src/runtime-adapters.test.ts+96−0
@@ -0,0 +1,96 @@
1import { describe, expect, it } from "@effect/vitest";
2import {
3 CanonicalToolDescriptorSchema,
4 type CanonicalToolDescriptor,
5} from "@executor-v2/schema";
6import * as Effect from "effect/Effect";
7import * as Either from "effect/Either";
8import * as Schema from "effect/Schema";
9
10import {
11 makeCloudflareWorkerLoaderRuntimeAdapter,
12 makeLocalInProcessRuntimeAdapter,
13 makeRuntimeAdapterRegistry,
14 RuntimeAdapterError,
15} from "./runtime-adapters";
16import {
17 makeToolProviderRegistry,
18 ToolProviderRegistryService,
19 type ToolProvider,
20} from "./tool-providers";
21
22const decodeCanonicalToolDescriptor = Schema.decodeUnknownSync(
23 CanonicalToolDescriptorSchema,
24);
25
26const sumToolDescriptor: CanonicalToolDescriptor = decodeCanonicalToolDescriptor({
27 providerKind: "in_memory",
28 sourceId: null,
29 workspaceId: null,
30 toolId: "sum",
31 name: "sum",
32 description: null,
33 invocationMode: "in_memory",
34 availability: "local_only",
35 providerPayload: null,
36});
37
38describe("runtime adapters", () => {
39 it.effect("executes with local-inproc adapter via runtime registry", () =>
40 Effect.gen(function* () {
41 const sumProvider: ToolProvider = {
42 kind: "in_memory",
43 invoke: (input) =>
44 Effect.gen(function* () {
45 const args = input.args as { a: number; b: number };
46 return {
47 output: args.a + args.b,
48 isError: false,
49 } as const;
50 }),
51 };
52
53 const toolRegistry = makeToolProviderRegistry([sumProvider]);
54 const runtimeRegistry = makeRuntimeAdapterRegistry([
55 makeLocalInProcessRuntimeAdapter(),
56 ]);
57
58 const result = yield* runtimeRegistry
59 .execute({
60 runtimeKind: "local-inproc",
61 code: "return await tools.sum({ a: 2, b: 4 });",
62 tools: [{ descriptor: sumToolDescriptor, source: null }],
63 })
64 .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry));
65
66 expect(result).toBe(6);
67 }),
68 );
69
70 it.effect("returns typed not-implemented error for cloudflare adapter", () =>
71 Effect.gen(function* () {
72 const toolRegistry = makeToolProviderRegistry([]);
73 const runtimeRegistry = makeRuntimeAdapterRegistry([
74 makeCloudflareWorkerLoaderRuntimeAdapter(),
75 ]);
76
77 const result = yield* Effect.either(
78 runtimeRegistry
79 .execute({
80 runtimeKind: "cloudflare-worker-loader",
81 code: "return 1;",
82 tools: [],
83 })
84 .pipe(Effect.provideService(ToolProviderRegistryService, toolRegistry)),
85 );
86
87 expect(Either.isLeft(result)).toBe(true);
88 if (Either.isLeft(result)) {
89 expect(result.left).toBeInstanceOf(RuntimeAdapterError);
90 if (result.left instanceof RuntimeAdapterError) {
91 expect(result.left.runtimeKind).toBe("cloudflare-worker-loader");
92 }
93 }
94 }),
95 );
96});
v2/packages/engine/src/runtime-adapters.ts+212−0
@@ -0,0 +1,212 @@
1import type { Source } from "@executor-v2/schema";
2import * as Context from "effect/Context";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import {
8 DenoSubprocessRunnerError,
9 executeJavaScriptInDenoSubprocess,
10} from "./deno-subprocess-runner";
11import {
12 executeJavaScriptWithTools,
13 LocalCodeRunnerError,
14} from "./local-runner";
15import {
16 ToolProviderError,
17 ToolProviderRegistryError,
18 type CanonicalToolDescriptor,
19 type ToolProviderRegistryService,
20} from "./tool-providers";
21
22export type RuntimeAdapterKind =
23 | "local-inproc"
24 | "deno-subprocess"
25 | "cloudflare-worker-loader";
26
27export type RuntimeRunnableTool = {
28 descriptor: CanonicalToolDescriptor;
29 source: Source | null;
30};
31
32export type RuntimeExecuteInput = {
33 code: string;
34 tools: ReadonlyArray<RuntimeRunnableTool>;
35 timeoutMs?: number;
36};
37
38export class RuntimeAdapterError extends Data.TaggedError("RuntimeAdapterError")<{
39 operation: string;
40 runtimeKind: RuntimeAdapterKind;
41 message: string;
42 details: string | null;
43}> {}
44
45export type RuntimeExecuteError =
46 | RuntimeAdapterError
47 | LocalCodeRunnerError
48 | DenoSubprocessRunnerError
49 | ToolProviderRegistryError
50 | ToolProviderError;
51
52export interface RuntimeAdapter {
53 readonly kind: RuntimeAdapterKind;
54 readonly isAvailable: () => Effect.Effect<boolean>;
55 readonly execute: (
56 input: RuntimeExecuteInput,
57 ) => Effect.Effect<unknown, RuntimeExecuteError, ToolProviderRegistryService>;
58}
59
60export class RuntimeAdapterRegistryError extends Data.TaggedError(
61 "RuntimeAdapterRegistryError",
62)<{
63 operation: string;
64 runtimeKind: RuntimeAdapterKind;
65 message: string;
66}> {}
67
68export interface RuntimeAdapterRegistry {
69 readonly register: (
70 adapter: RuntimeAdapter,
71 ) => Effect.Effect<void, RuntimeAdapterRegistryError>;
72
73 readonly registerAll: (
74 adapters: ReadonlyArray<RuntimeAdapter>,
75 ) => Effect.Effect<void, RuntimeAdapterRegistryError>;
76
77 readonly get: (
78 runtimeKind: RuntimeAdapterKind,
79 ) => Effect.Effect<RuntimeAdapter, RuntimeAdapterRegistryError>;
80
81 readonly execute: (
82 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
83 ) => Effect.Effect<unknown, RuntimeExecuteError | RuntimeAdapterRegistryError, ToolProviderRegistryService>;
84}
85
86export class RuntimeAdapterRegistryService extends Context.Tag(
87 "@executor-v2/engine/RuntimeAdapterRegistryService",
88)<RuntimeAdapterRegistryService, RuntimeAdapterRegistry>() {}
89
90type DenoSubprocessRuntimeAdapterOptions = {
91 denoExecutable?: string;
92 defaultTimeoutMs?: number;
93};
94
95const duplicateRuntimeAdapterError = (
96 runtimeKind: RuntimeAdapterKind,
97): RuntimeAdapterRegistryError =>
98 new RuntimeAdapterRegistryError({
99 operation: "register",
100 runtimeKind,
101 message: `Runtime adapter already registered: ${runtimeKind}`,
102 });
103
104const runtimeAdapterNotFoundError = (
105 operation: string,
106 runtimeKind: RuntimeAdapterKind,
107): RuntimeAdapterRegistryError =>
108 new RuntimeAdapterRegistryError({
109 operation,
110 runtimeKind,
111 message: `No runtime adapter registered for kind: ${runtimeKind}`,
112 });
113
114export const makeLocalInProcessRuntimeAdapter = (): RuntimeAdapter => ({
115 kind: "local-inproc",
116 isAvailable: () => Effect.succeed(true),
117 execute: (input) =>
118 executeJavaScriptWithTools({
119 code: input.code,
120 tools: input.tools,
121 }),
122});
123
124export const makeDenoSubprocessRuntimeAdapter = (
125 options: DenoSubprocessRuntimeAdapterOptions = {},
126): RuntimeAdapter => ({
127 kind: "deno-subprocess",
128 isAvailable: () => Effect.succeed(true),
129 execute: (input) =>
130 executeJavaScriptInDenoSubprocess({
131 code: input.code,
132 tools: input.tools,
133 timeoutMs: input.timeoutMs ?? options.defaultTimeoutMs,
134 denoExecutable: options.denoExecutable,
135 }),
136});
137
138export const makeCloudflareWorkerLoaderRuntimeAdapter = (): RuntimeAdapter => ({
139 kind: "cloudflare-worker-loader",
140 isAvailable: () => Effect.succeed(false),
141 execute: () =>
142 new RuntimeAdapterError({
143 operation: "execute",
144 runtimeKind: "cloudflare-worker-loader",
145 message: "Cloudflare worker loader runtime adapter is not implemented yet",
146 details: null,
147 }),
148});
149
150export const makeRuntimeAdapterRegistry = (
151 initialAdapters: ReadonlyArray<RuntimeAdapter> = [],
152): RuntimeAdapterRegistry => {
153 const adaptersByKind = new Map<RuntimeAdapterKind, RuntimeAdapter>();
154
155 const register = (
156 adapter: RuntimeAdapter,
157 ): Effect.Effect<void, RuntimeAdapterRegistryError> =>
158 Effect.gen(function* () {
159 if (adaptersByKind.has(adapter.kind)) {
160 return yield* duplicateRuntimeAdapterError(adapter.kind);
161 }
162
163 adaptersByKind.set(adapter.kind, adapter);
164 });
165
166 const registerAll = (
167 adapters: ReadonlyArray<RuntimeAdapter>,
168 ): Effect.Effect<void, RuntimeAdapterRegistryError> =>
169 Effect.forEach(adapters, register, { discard: true });
170
171 const get = (
172 runtimeKind: RuntimeAdapterKind,
173 ): Effect.Effect<RuntimeAdapter, RuntimeAdapterRegistryError> =>
174 Effect.gen(function* () {
175 const adapter = adaptersByKind.get(runtimeKind);
176 if (!adapter) {
177 return yield* runtimeAdapterNotFoundError("get", runtimeKind);
178 }
179
180 return adapter;
181 });
182
183 const execute = (
184 input: RuntimeExecuteInput & { runtimeKind: RuntimeAdapterKind },
185 ): Effect.Effect<
186 unknown,
187 RuntimeExecuteError | RuntimeAdapterRegistryError,
188 ToolProviderRegistryService
189 > =>
190 Effect.gen(function* () {
191 const adapter = yield* get(input.runtimeKind);
192 return yield* adapter.execute(input);
193 });
194
195 for (const adapter of initialAdapters) {
196 if (!adaptersByKind.has(adapter.kind)) {
197 adaptersByKind.set(adapter.kind, adapter);
198 }
199 }
200
201 return {
202 register,
203 registerAll,
204 get,
205 execute,
206 };
207};
208
209export const RuntimeAdapterRegistryLive = (
210 initialAdapters: ReadonlyArray<RuntimeAdapter> = [],
211): Layer.Layer<RuntimeAdapterRegistryService> =>
212 Layer.succeed(RuntimeAdapterRegistryService, makeRuntimeAdapterRegistry(initialAdapters));
v2/packages/engine/src/tool-providers.test.ts+83−86
@@ -1,6 +1,7 @@
1import { describe, expect, test } from "bun:test";1import { describe, expect, it } from "@effect/vitest";
2import { type Source, SourceSchema } from "@executor-v2/schema";2import { type Source, SourceSchema } from "@executor-v2/schema";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Either from "effect/Either";
4import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
5 6
6import {7import {
@@ -29,103 +30,99 @@ const createOpenApiSource = (): Source => · 22 unmodified lines
29 });30 });
30 31
31describe("makeToolProviderRegistry", () => {32describe("makeToolProviderRegistry", () => {
32 test("routes discovery and invocation through provider kind", async () => {33 it.effect("routes discovery and invocation through provider kind", () =>
33 const openApiProvider: ToolProvider = {34 Effect.gen(function* () {
34 kind: "openapi",35 const openApiProvider: ToolProvider = {
35 discoverFromSource: (source) =>36 kind: "openapi",
36 Effect.succeed({37 discoverFromSource: (source) =>
37 sourceHash: "hash_1",38 Effect.succeed({
38 tools: [39 sourceHash: "hash_1",
39 {40 tools: [
40 providerKind: "openapi",41 {
41 sourceId: source.id,42 providerKind: "openapi",
42 workspaceId: source.workspaceId,43 sourceId: source.id,
43 toolId: "getHealth",44 workspaceId: source.workspaceId,
44 name: "Get health",45 toolId: "getHealth",
45 description: null,46 name: "Get health",
46 invocationMode: "http",47 description: null,
47 availability: "remote_capable",48 invocationMode: "http",
48 providerPayload: {49 availability: "remote_capable",
49 method: "get",50 providerPayload: {
50 pathTemplate: "/healthz",51 method: "get",
51 parameters: [],52 pathTemplate: "/healthz",
52 requestBody: null,53 parameters: [],
54 requestBody: null,
55 },
53 },56 },
54 },57 ],
55 ],58 }),
56 }),59 invoke: () =>
57 invoke: () =>60 Effect.succeed({
58 Effect.succeed({61 output: { ok: true },
59 output: { ok: true },62 isError: false,
60 isError: false,63 }),
61 }),64 };
62 };
63
64 const registry = makeToolProviderRegistry([openApiProvider]);
65
66 const source = createOpenApiSource();
67 const discovery = await Effect.runPromise(registry.discoverFromSource(source));
68
69 expect(discovery.tools).toHaveLength(1);
70 expect(discovery.tools[0]?.toolId).toBe("getHealth");
71
72 const result = await Effect.runPromise(
73 registry.invoke({
74 source,
75 tool: discovery.tools[0]!,
76 args: { verbose: true },
77 }),
78 );
79
80 expect(result.isError).toBe(false);
81 expect(result.output).toEqual({ ok: true });
82 });
83
84 test("fails on duplicate provider registration", async () => {
85 const openApiProvider: ToolProvider = {
86 kind: "openapi",
87 invoke: () => Effect.succeed({ output: null, isError: false }),
88 };
89 65
90 const registry = makeToolProviderRegistry([openApiProvider]);66 const registry = makeToolProviderRegistry([openApiProvider]);
91 const result = await Effect.runPromise(
92 Effect.either(registry.register(openApiProvider)),
93 );
94 67
95 if (result._tag === "Right") {68 const source = createOpenApiSource();
96 throw new Error("expected duplicate provider registration to fail");69 const discovery = yield* registry.discoverFromSource(source);
97 }
98 70
99 expect(result.left).toBeInstanceOf(ToolProviderRegistryError);71 expect(discovery.tools).toHaveLength(1);
100 expect(result.left.message).toContain("Provider already registered");72 expect(discovery.tools[0]?.toolId).toBe("getHealth");
101 });
102 73
103 test("fails when provider cannot discover from source", async () => {74 const result = yield* registry.invoke({
104 const mcpProvider: ToolProvider = {75 source,
105 kind: "mcp",76 tool: discovery.tools[0]!,
106 invoke: () =>77 args: { verbose: true },
107 Effect.fail(78 });
79
80 expect(result.isError).toBe(false);
81 expect(result.output).toEqual({ ok: true });
82 }),
83 );
84
85 it.effect("fails on duplicate provider registration", () =>
86 Effect.gen(function* () {
87 const openApiProvider: ToolProvider = {
88 kind: "openapi",
89 invoke: () => Effect.succeed({ output: null, isError: false }),
90 };
91
92 const registry = makeToolProviderRegistry([openApiProvider]);
93 const result = yield* Effect.either(registry.register(openApiProvider));
94
95 expect(Either.isLeft(result)).toBe(true);
96 if (Either.isLeft(result)) {
97 expect(result.left).toBeInstanceOf(ToolProviderRegistryError);
98 expect(result.left.message).toContain("Provider already registered");
99 }
100 }),
101 );
102
103 it.effect("fails when provider cannot discover from source", () =>
104 Effect.gen(function* () {
105 const mcpProvider: ToolProvider = {
106 kind: "mcp",
107 invoke: () =>
108 new ToolProviderError({108 new ToolProviderError({
109 operation: "invoke",109 operation: "invoke",
110 providerKind: "mcp",110 providerKind: "mcp",
111 message: "not implemented",111 message: "not implemented",
112 details: null,112 details: null,
113 }),113 }),
114 ),114 };
115 };
116
117 const registry = makeToolProviderRegistry([mcpProvider]);
118 const source = createOpenApiSource();
119 115
120 const result = await Effect.runPromise(116 const registry = makeToolProviderRegistry([mcpProvider]);
121 Effect.either(registry.discoverFromSource(source)),117 const source = createOpenApiSource();
122 );
123 118
124 if (result._tag === "Right") {119 const result = yield* Effect.either(registry.discoverFromSource(source));
125 throw new Error("expected discovery to fail without provider");
126 }
127 120
128 expect(result.left).toBeInstanceOf(ToolProviderRegistryError);121 expect(Either.isLeft(result)).toBe(true);
129 expect(result.left.message).toContain("No provider registered");122 if (Either.isLeft(result)) {
130 });123 expect(result.left).toBeInstanceOf(ToolProviderRegistryError);
124 expect(result.left.message).toContain("No provider registered");
125 }
126 }),
127 );
131});128});
v2/packages/mcp-gateway/README.md+1−1
@@ -5,4 +5,4 @@ Bare-minimum MCP gateway for Executor v2. · 4 unmodified lines
5Current scaffold includes:5Current scaffold includes:
6- MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts`6- MCP server built with `@modelcontextprotocol/sdk` in `src/server.ts`
7- streamable HTTP request handling (`handleMcpHttpRequest`)7- streamable HTTP request handling (`handleMcpHttpRequest`)
8- one stub tool: `executor.ping`8- tools: `executor.ping` and `executor.execute` (the execute tool is host-wired via callback)
v2/packages/mcp-gateway/src/index.ts+3−0
@@ -1,5 +1,8 @@
1export {1export {
2 handleMcpHttpRequest,2 handleMcpHttpRequest,
3 type ExecuteToolInput,
4 type ExecuteToolResult,
5 type GatewayRuntimeKind,
3 type GatewayTarget,6 type GatewayTarget,
4 type McpGatewayOptions,7 type McpGatewayOptions,
5} from "./server";8} from "./server";
v2/packages/mcp-gateway/src/server.ts+83−0
@@ -4,20 +4,57 @@ import { z } from "zod"; · 3 unmodified lines
4 4
5export type GatewayTarget = "local" | "remote";5export type GatewayTarget = "local" | "remote";
6 6
7export type GatewayRuntimeKind =
8 | "local-inproc"
9 | "deno-subprocess"
10 | "cloudflare-worker-loader";
11
12export type ExecuteToolInput = {
13 code: string;
14 runtimeKind?: GatewayRuntimeKind;
15};
16
17export type ExecuteToolResult = {
18 output?: unknown;
19 error?: string;
20 isError: boolean;
21};
22
7export type McpGatewayOptions = {23export type McpGatewayOptions = {
8 target: GatewayTarget;24 target: GatewayTarget;
9 serverName?: string;25 serverName?: string;
10 serverVersion?: string;26 serverVersion?: string;
27 execute?: (input: ExecuteToolInput) => Promise<ExecuteToolResult>;
11};28};
12 29
13const DEFAULT_SERVER_NAME = "executor-v2";30const DEFAULT_SERVER_NAME = "executor-v2";
14const DEFAULT_SERVER_VERSION = "0.0.0";31const DEFAULT_SERVER_VERSION = "0.0.0";
15const STUB_TOOL_NAME = "executor.ping";32const STUB_TOOL_NAME = "executor.ping";
33const EXECUTE_TOOL_NAME = "executor.execute";
16 34
17const PingToolInput = z.object({35const PingToolInput = z.object({
18 message: z.string().optional(),36 message: z.string().optional(),
19});37});
20 38
39const ExecuteToolInputSchema = z.object({
40 code: z.string(),
41 runtimeKind: z
42 .enum(["local-inproc", "deno-subprocess", "cloudflare-worker-loader"])
43 .optional(),
44});
45
46const contentText = (value: unknown): string => {
47 if (typeof value === "string") {
48 return value;
49 }
50
51 try {
52 return JSON.stringify(value, null, 2);
53 } catch {
54 return String(value);
55 }
56};
57
21const createStubMcpServer = (options: McpGatewayOptions): McpServer => {58const createStubMcpServer = (options: McpGatewayOptions): McpServer => {
22 const mcp = new McpServer({59 const mcp = new McpServer({
23 name: options.serverName ?? DEFAULT_SERVER_NAME,60 name: options.serverName ?? DEFAULT_SERVER_NAME,
@@ -47,6 +84,52 @@ const createStubMcpServer = (options: McpGatewayOptions): McpServer => { · 23 unmodified lines
47 },84 },
48 );85 );
49 86
87 mcp.registerTool(
88 EXECUTE_TOOL_NAME,
89 {
90 description: "Execute JavaScript against configured runtime adapter",
91 inputSchema: ExecuteToolInputSchema,
92 },
93 async (input: ExecuteToolInput) => {
94 if (!options.execute) {
95 return {
96 content: [
97 {
98 type: "text" as const,
99 text: "executor.execute is not configured for this gateway target",
100 },
101 ],
102 isError: true,
103 };
104 }
105
106 try {
107 const result = await options.execute(input);
108 return {
109 content: [
110 {
111 type: "text" as const,
112 text: result.isError
113 ? result.error ?? "Execution failed"
114 : contentText(result.output),
115 },
116 ],
117 isError: result.isError,
118 };
119 } catch (cause) {
120 return {
121 content: [
122 {
123 type: "text" as const,
124 text: cause instanceof Error ? cause.message : String(cause),
125 },
126 ],
127 isError: true,
128 };
129 }
130 },
131 );
132
50 return mcp;133 return mcp;
51};134};
52 135
v2/packages/oauth/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/rpc/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/sdk/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/secrets/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};
v2/packages/source-manager/package.json+5−0
@@ -6,11 +6,16 @@ · 5 unmodified lines
6 ".": "./src/index.ts"6 ".": "./src/index.ts"
7 },7 },
8 "scripts": {8 "scripts": {
9 "test": "bunx vitest run --no-watch src/*.test.ts",
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json"10 "typecheck": "bunx tsc --noEmit -p tsconfig.json"
10 },11 },
11 "dependencies": {12 "dependencies": {
12 "@executor-v2/persistence-ports": "workspace:*",13 "@executor-v2/persistence-ports": "workspace:*",
13 "@executor-v2/schema": "workspace:*",14 "@executor-v2/schema": "workspace:*",
14 "effect": "^3.19.8"15 "effect": "^3.19.8"
16 },
17 "devDependencies": {
18 "@effect/vitest": "^0.27.0",
19 "vitest": "^3.2.4"
15 }20 }
16}21}
v2/packages/source-manager/src/openapi-extraction.test.ts+55−55
@@ -1,73 +1,73 @@
1import { describe, expect, test } from "bun:test";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3 3
4import { extractOpenApiManifest } from "./openapi-extraction";4import { extractOpenApiManifest } from "./openapi-extraction";
5 5
6describe("extractOpenApiManifest invocation metadata", () => {6describe("extractOpenApiManifest invocation metadata", () => {
7 test("extracts parameter and request body metadata", async () => {7 it.effect("extracts parameter and request body metadata", () =>
8 const openApiSpec = {8 Effect.gen(function* () {
9 openapi: "3.1.0",9 const openApiSpec = {
10 paths: {10 openapi: "3.1.0",
11 "/users/{userId}": {11 paths: {
12 parameters: [12 "/users/{userId}": {
13 { name: "userId", in: "path" },
14 { name: "x-org-id", in: "header", required: true },
15 ],
16 get: {
17 operationId: "getUser",
18 summary: "Get user",
19 parameters: [13 parameters: [
20 { name: "include", in: "query" },14 { name: "userId", in: "path" },
21 { name: "x-org-id", in: "header", required: false },15 { name: "x-org-id", in: "header", required: true },
22 ],16 ],
23 },17 get: {
24 post: {18 operationId: "getUser",
25 summary: "Update user",19 summary: "Get user",
26 requestBody: {20 parameters: [
27 required: true,21 { name: "include", in: "query" },
28 content: {22 { name: "x-org-id", in: "header", required: false },
29 "application/merge-patch+json": {},23 ],
30 "application/json": {},24 },
25 post: {
26 summary: "Update user",
27 requestBody: {
28 required: true,
29 content: {
30 "application/merge-patch+json": {},
31 "application/json": {},
32 },
31 },33 },
32 },34 },
33 },35 },
34 },36 },
35 },37 };
36 };
37 38
38 const manifest = await Effect.runPromise(39 const manifest = yield* extractOpenApiManifest("test-source", openApiSpec);
39 extractOpenApiManifest("test-source", openApiSpec),
40 );
41 40
42 const getTool = manifest.tools.find((tool) => tool.toolId === "getUser");41 const getTool = manifest.tools.find((tool) => tool.toolId === "getUser");
43 expect(getTool).toBeDefined();42 expect(getTool).toBeDefined();
44 if (!getTool) {43 if (!getTool) {
45 throw new Error("expected getUser tool");44 throw new Error("expected getUser tool");
46 }45 }
47 46
48 expect(getTool.invocation.method).toBe("get");47 expect(getTool.invocation.method).toBe("get");
49 expect(getTool.invocation.pathTemplate).toBe("/users/{userId}");48 expect(getTool.invocation.pathTemplate).toBe("/users/{userId}");
50 expect(getTool.invocation.requestBody).toBeNull();49 expect(getTool.invocation.requestBody).toBeNull();
51 50
52 expect(getTool.invocation.parameters).toEqual(51 expect(getTool.invocation.parameters).toEqual(
53 expect.arrayContaining([52 expect.arrayContaining([
54 { name: "userId", location: "path", required: true },53 { name: "userId", location: "path", required: true },
55 { name: "x-org-id", location: "header", required: false },54 { name: "x-org-id", location: "header", required: false },
56 { name: "include", location: "query", required: false },55 { name: "include", location: "query", required: false },
57 ]),56 ]),
58 );57 );
59 58
60 const postTool = manifest.tools.find(59 const postTool = manifest.tools.find(
61 (tool) => tool.toolId === "post_users_userid",60 (tool) => tool.toolId === "post_users_userid",
62 );61 );
63 expect(postTool).toBeDefined();62 expect(postTool).toBeDefined();
64 if (!postTool) {63 if (!postTool) {
65 throw new Error("expected post tool");64 throw new Error("expected post tool");
66 }65 }
67 66
68 expect(postTool.invocation.requestBody).toEqual({67 expect(postTool.invocation.requestBody).toEqual({
69 required: true,68 required: true,
70 contentTypes: ["application/json", "application/merge-patch+json"],69 contentTypes: ["application/json", "application/merge-patch+json"],
71 });70 });
72 });71 }),
72 );
73});73});
v2/packages/source-manager/src/openapi-extraction.ts+12−12
@@ -45,17 +45,17 @@ export class OpenApiExtractionError extends Data.TaggedError("OpenApiExtractionE · 44 unmodified lines
45 details: string | null;45 details: string | null;
46}> {}46}> {}
47 47
48export const ExtractedToolParameterSchema = OpenApiToolParameterSchema;48const ExtractedToolParameterSchema = OpenApiToolParameterSchema;
49export const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema;49const ExtractedToolRequestBodySchema = OpenApiToolRequestBodySchema;
50export const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema;50const ExtractedToolInvocationSchema = OpenApiInvocationPayloadSchema;
51export const ExtractedToolSchema = OpenApiExtractedToolSchema;51const ExtractedToolSchema = OpenApiExtractedToolSchema;
52export const ToolManifestSchema = OpenApiToolManifestSchema;52const ToolManifestSchema = OpenApiToolManifestSchema;
53 53
54export type ExtractedToolParameter = OpenApiToolParameter;54type ExtractedToolParameter = OpenApiToolParameter;
55export type ExtractedToolRequestBody = OpenApiToolRequestBody;55type ExtractedToolRequestBody = OpenApiToolRequestBody;
56export type ExtractedToolInvocation = OpenApiInvocationPayload;56type ExtractedToolInvocation = OpenApiInvocationPayload;
57export type ExtractedTool = OpenApiExtractedTool;57type ExtractedTool = OpenApiExtractedTool;
58export type ToolManifest = OpenApiToolManifest;58type ToolManifest = OpenApiToolManifest;
59 59
60const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema);60const ToolManifestFromJsonSchema = Schema.parseJson(ToolManifestSchema);
61const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema);61const encodeManifestToJson = Schema.encode(ToolManifestFromJsonSchema);
@@ -75,7 +75,7 @@ export type RefreshOpenApiArtifactResult = { · 13 unmodified lines
75 reused: boolean;75 reused: boolean;
76};76};
77 77
78export type RefreshOpenApiArtifactInput = {78type RefreshOpenApiArtifactInput = {
79 source: Source;79 source: Source;
80 openApiSpec: unknown;80 openApiSpec: unknown;
81 artifactStore: ToolArtifactStore;81 artifactStore: ToolArtifactStore;
v2/packages/sync/src/index.ts+1−0
@@ -0,0 +1 @@
1export {};