| @@ -1,6 +1,7 @@ |
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; | 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; |
| | 4 | import { Effect } from "effect"; |
| 4 | import { mkdtempSync } from "node:fs"; | 5 | import { mkdtempSync } from "node:fs"; |
| 5 | import { tmpdir } from "node:os"; | 6 | import { tmpdir } from "node:os"; |
| 6 | import { join, resolve } from "node:path"; | 7 | import { join, resolve } from "node:path"; |
| @@ -10,35 +11,39 @@ const cliEntry = resolve(repoRoot, "apps/cli/src/main.ts"); · 3 unmodified lines |
| 10 | const testScope = resolve(repoRoot, "apps/local"); | 11 | const testScope = resolve(repoRoot, "apps/local"); |
| 11 | | 12 | |
| 12 | describe("MCP stdio integration", () => { | 13 | describe("MCP stdio integration", () => { |
| 13 | it("execute tool returns result over stdio transport", async () => { | 14 | it.effect("execute tool returns result over stdio transport", () => |
| 14 | // Fresh temp dir so the test doesn't migrate against the developer's | 15 | Effect.gen(function* () { |
| 15 | // real ~/.executor/data.db. | 16 | // Fresh temp dir so the test doesn't migrate against the developer's |
| 16 | const dataDir = mkdtempSync(join(tmpdir(), "executor-mcp-test-")); | 17 | // real ~/.executor/data.db. |
| 17 | | 18 | const dataDir = mkdtempSync(join(tmpdir(), "executor-mcp-test-")); |
| 18 | const transport = new StdioClientTransport({ | 19 | |
| 19 | command: "bun", | 20 | const transport = new StdioClientTransport({ |
| 20 | args: ["run", cliEntry, "mcp", "--scope", testScope], | 21 | command: "bun", |
| 21 | env: { ...process.env, EXECUTOR_DATA_DIR: dataDir }, | 22 | args: ["run", cliEntry, "mcp", "--scope", testScope], |
| 22 | }); | 23 | env: { ...process.env, EXECUTOR_DATA_DIR: dataDir }, |
| | 24 | }); |
| 23 | | 25 | |
| 24 | const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }); | 26 | const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }); |
| 25 | | 27 | |
| 26 | await client.connect(transport); | 28 | yield* Effect.acquireRelease( |
| | 29 | Effect.promise(() => client.connect(transport)), |
| | 30 | () => Effect.promise(() => transport.close()), |
| | 31 | ); |
| 27 | | 32 | |
| 28 | try { | 33 | const { tools } = yield* Effect.promise(() => client.listTools()); |
| 29 | const { tools } = await client.listTools(); | | |
| 30 | expect(tools.map((t) => t.name)).toContain("execute"); | 34 | expect(tools.map((t) => t.name)).toContain("execute"); |
| 31 | | 35 | |
| 32 | const result = await client.callTool({ | 36 | const result = yield* Effect.promise(() => |
| 33 | name: "execute", | 37 | client.callTool({ |
| 34 | arguments: { code: "return 2+2" }, | 38 | name: "execute", |
| 35 | }); | 39 | arguments: { code: "return 2+2" }, |
| | 40 | }), |
| | 41 | ); |
| 36 | | 42 | |
| 37 | const text = (result.content as Array<{ type: string; text: string }>)[0]?.text; | 43 | const text = (result.content as Array<{ type: string; text: string }>)[0]?.text; |
| 38 | expect(text).toContain("4"); | 44 | expect(text).toContain("4"); |
| 39 | expect(result.isError).toBeFalsy(); | 45 | expect(result.isError).toBeFalsy(); |
| 40 | } finally { | 46 | }).pipe(Effect.scoped), |
| 41 | await transport.close(); | 47 | { timeout: 30_000 }, |
| 42 | } | 48 | ); |
| 43 | }, 30_000); | | |
| 44 | }); | 49 | }); |