From e31131555651d9da971048a97de6a465e7013f26 Mon Sep 17 00:00:00 2001 From: elpresidank Date: Tue, 2 Jun 2026 08:59:53 -0500 Subject: [PATCH] Add Effect stdio MCP entrypoint --- ts/EFFECT_NATIVE_REWRITE_AUDIT.md | 52 +++++++++++++++---- ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md | 7 ++- ts/packages/mcp/package.json | 2 +- .../mcp/src/__tests__/server-effect.test.ts | 51 ++++++++++++++++++ ts/packages/mcp/src/server-effect.ts | 40 ++++++++++++-- 5 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 ts/packages/mcp/src/__tests__/server-effect.test.ts diff --git a/ts/EFFECT_NATIVE_REWRITE_AUDIT.md b/ts/EFFECT_NATIVE_REWRITE_AUDIT.md index 8e6473be..b816fc21 100644 --- a/ts/EFFECT_NATIVE_REWRITE_AUDIT.md +++ b/ts/EFFECT_NATIVE_REWRITE_AUDIT.md @@ -1477,13 +1477,45 @@ Notes: - `cd ts && bun run lint` - `git diff --check` +### 2026-06-02: MCP Effect Stdio Entrypoint Slice + +- Status: migrated and root-verified. +- Completed: + - Added an Effect-native stdio layer and process entrypoint with + `McpServer.layerStdio`, `NodeStdio.layer`, and `NodeRuntime.runMain`. + - Reused the same `TrustGraphMcpToolkitLive` path for HTTP and stdio through + a shared toolkit-layer helper. + - Kept the legacy SDK/Zod stdio export as a compatibility surface until + protocol-level `tools/list` and `tools/call` parity tests prove it can be + flipped or removed. + - Added focused coverage that the Effect toolkit names remain stable and + the stdio layer/entrypoint are exported. + - Updated the MCP test script to ignore compiled `dist/**` output so root + builds do not cause duplicate Vitest runs from generated tests. +- Scratch-note triage: + - Metrics, in-process PubSub fanout, Claude Effect AI, RPC + `S.TaggedErrorClass`, and `@effect/tsgo` setup are already migrated. + - Remaining valid scratch targets are MCP protocol parity/flip, Duration + config cleanup, Term/ClientTerm tagged-union matching, service + `Effect.fn` normalization, `@effect/cli`, stream/RPC follow-ups, chunking + `Chunk`, cores Promise APIs, and long-lived `Map`/`Set` state. +- Verification: + - `cd ts/packages/mcp && bun run test` + - `cd ts/packages/mcp && bun run build` + - `cd ts && bun run check:tsgo` + - `cd ts && bun run build` + - `cd ts && bun run test` + - `cd ts && bun run lint` + - `git diff --check` + ## Subagent Findings To Preserve - MCP/workbench: - - Make the Effect MCP server the canonical implementation. The old stdio - server should remain only as compatibility while parity tests and an - Effect `McpServer.layerStdio` entrypoint are missing. Do not delete - `server.ts` until stdio `tools/list`/`tools/call` parity is proved. + - Make the Effect MCP server the canonical implementation. An Effect + `McpServer.layerStdio` entrypoint now exists; the old stdio server should + remain only as compatibility until protocol-level `tools/list` and + `tools/call` parity is proved. Do not delete `server.ts` until that parity + coverage exists, with special attention to `text_completion` behavior. - Workbench BaseApi atoms can move toward `AtomRpc` or `AtomHttpApi` after the client API is less Promise-first. - MCP env is now Config-backed; continue that policy for future MCP settings. @@ -1701,13 +1733,13 @@ Notes: ### P2: Canonicalize MCP Around The Effect Server - Status: - - First blocker slice complete: MCP now builds under strict tsgo and the - stdio server has an Effect-backed compatibility implementation. + - MCP now builds under strict tsgo, the stdio server has an Effect-backed + compatibility implementation, and an Effect `McpServer.layerStdio` + entrypoint exists. - Remaining shape: - Keep the old SDK/Zod stdio compatibility surface for now. - - Add an Effect stdio entrypoint with `McpServer.layerStdio`, then prove - `tools/list` and `tools/call` parity before deleting any public entry - point or dropping `zod`/server-side MCP SDK dependencies. + - Prove `tools/list` and `tools/call` parity before deleting any public + entry point or dropping `zod`/server-side MCP SDK dependencies. - Pay special attention to `text_completion`: legacy calls the TrustGraph gateway, while the Effect server currently uses an Effect AI `LanguageModel`/OpenAI layer. @@ -1733,7 +1765,7 @@ Notes: ## Recommended PR Order -1. MCP Effect stdio parity and canonicalization. +1. MCP protocol parity tests and legacy stdio flip/removal decision. 2. Term/ClientTerm Schema tagged-union and Match normalization. 3. FlowManager/service `Effect.fn` normalization. 4. Messaging runtime `Config.duration` / `Duration` cleanup. diff --git a/ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md b/ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md index 001ef649..67048f3e 100644 --- a/ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md +++ b/ts/EFFECT_NATIVE_REWRITE_PLAYBOOK.md @@ -58,6 +58,7 @@ primitive exists. | Promise loops, top-level async orchestration | `Effect`, `Effect.fn`, `Effect.scoped`, `Effect.runPromiseWith` at boundaries | `effect` | `packages/effect/src/Effect.ts` | | Resource construction and teardown | `Layer`, `Scope`, `Effect.acquireRelease`, `Effect.addFinalizer` | `effect` | `packages/effect/src/Layer.ts`, `packages/effect/src/Scope.ts` | | Mutable service state | `Ref`, `SynchronizedRef`, `SubscriptionRef` | `effect` | `packages/effect/src/Ref.ts`, `packages/effect/src/SynchronizedRef.ts` | +| Long-lived keyed state and set membership | `HashMap`, `MutableHashMap`, `HashSet`, `MutableHashSet` | `effect` | `packages/effect/src/HashMap.ts`, `packages/effect/src/MutableHashMap.ts`, `packages/effect/src/HashSet.ts`, `packages/effect/src/MutableHashSet.ts` | | Polling, delays, retry/backoff | `Schedule`, `Effect.sleep`, `Effect.retry` | `effect` | `packages/effect/src/Schedule.ts`, `packages/effect/src/Effect.ts` | | Callback queues and streaming fanout | `Queue`, `PubSub`, `Stream`, `Channel` | `effect` | `packages/effect/src/Queue.ts`, `packages/effect/src/PubSub.ts`, `packages/effect/src/Stream.ts`, `packages/effect/src/Channel.ts` | | Env/config decoding | `Config`, `ConfigProvider`, platform config providers | `effect`, `effect/ConfigProvider`, provider packages | `packages/effect/src/Config.ts`, `packages/effect/src/ConfigProvider.ts`, `packages/platform/src/PlatformConfigProvider.ts` | @@ -95,6 +96,10 @@ Known concrete exports useful to scouts: - `Effect.try`, `Effect.tryPromise`, and `Result.try` for exception capture. Treat `try`/`catch` blocks as migration evidence unless they are host/tool boundaries or test-only helpers. +- `S.toTaggedUnion(...).match` and `effect/Match` discriminator helpers for + discriminated unions. Treat native `switch` statements as migration evidence + unless the code is a host parser/traversal boundary with no useful Effect + schema or match primitive. ## Scout Workflow @@ -112,7 +117,7 @@ Known concrete exports useful to scouts: 4. Run quick signal scans: ```sh - rg -n "try \\{|new Error|new Promise|setTimeout|while \\(|receive\\(|Effect\\.runPromise|toPromiseRequestor|makeAsyncProcessor|process\\.env|JSON\\.parse|JSON\\.stringify|localStorage|new Map|WebSocket" ts/packages --glob '*.ts' --glob '*.tsx' + rg -n "S\\.ErrorClass|try \\{|catch \\(|new Error|new Promise|setTimeout|while \\(|switch \\(|receive\\(|Effect\\.runPromise|toPromiseRequestor|makeAsyncProcessor|process\\.env|JSON\\.parse|JSON\\.stringify|localStorage|new Map|new Set|Map<|Set<|WebSocket" ts/packages --glob '*.ts' --glob '*.tsx' ``` 5. Split scouts by lane. If the thread cannot spawn every scout in parallel, diff --git a/ts/packages/mcp/package.json b/ts/packages/mcp/package.json index 2d0b4c1a..405a9990 100644 --- a/ts/packages/mcp/package.json +++ b/ts/packages/mcp/package.json @@ -8,7 +8,7 @@ "build": "bunx --bun tsc", "dev": "tsc --watch", "clean": "rm -rf dist", - "test": "bunx --bun vitest run --passWithNoTests" + "test": "bunx --bun vitest run --passWithNoTests --exclude=dist/**" }, "dependencies": { "@trustgraph/base": "workspace:*", diff --git a/ts/packages/mcp/src/__tests__/server-effect.test.ts b/ts/packages/mcp/src/__tests__/server-effect.test.ts new file mode 100644 index 00000000..6778c173 --- /dev/null +++ b/ts/packages/mcp/src/__tests__/server-effect.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { + makeTrustGraphMcpStdioLayer, + runStdio, + TrustGraphMcpToolkit, +} from "../server-effect.js"; + +const expectedToolNames = [ + "text_completion", + "graph_rag", + "document_rag", + "agent", + "embeddings", + "triples_query", + "graph_embeddings_query", + "get_config_all", + "get_config", + "put_config", + "delete_config", + "get_flows", + "get_flow", + "start_flow", + "stop_flow", + "get_documents", + "load_document", + "remove_document", + "get_prompts", + "get_prompt", + "get_knowledge_cores", + "delete_kg_core", + "load_kg_core", +]; + +describe("Effect MCP server", () => { + it("keeps the canonical Effect toolkit names stable", () => { + expect(Object.keys(TrustGraphMcpToolkit.tools)).toEqual(expectedToolNames); + }); + + it("exposes an Effect stdio layer and process entrypoint", () => { + expect( + makeTrustGraphMcpStdioLayer({ + gatewayUrl: "ws://localhost:8088/api/v1/rpc", + user: "mcp-test", + flowId: "default", + openAiApiKey: "test-key", + }), + ).toBeDefined(); + + expect(runStdio).toEqual(expect.any(Function)); + }); +}); diff --git a/ts/packages/mcp/src/server-effect.ts b/ts/packages/mcp/src/server-effect.ts index 68e0a185..fe455491 100644 --- a/ts/packages/mcp/src/server-effect.ts +++ b/ts/packages/mcp/src/server-effect.ts @@ -1,5 +1,6 @@ import {OpenAiClient, OpenAiLanguageModel} from "@effect/ai-openai"; import {BunHttpServer, BunRuntime} from "@effect/platform-bun"; +import {NodeRuntime, NodeStdio} from "@effect/platform-node"; import {createTrustGraphSocket, type BaseApi, type Term as ClientTerm} from "@trustgraph/client"; import {Config, Context, Effect, Layer, Redacted} from "effect"; import * as O from "effect/Option"; @@ -1725,10 +1726,7 @@ export const TrustGraphMcpHttpApiRoutes = HttpApiBuilder.layer( const makeTrustGraphMcpHttpLayerFromConfig = ( config: TrustGraphMcpConfigShape, ) => { - const tools = McpServer.toolkit(TrustGraphMcpToolkit).pipe( - Layer.provide(TrustGraphMcpToolkitLive), - Layer.provide(makeOpenAiProviderLayerFromConfig(config)), - ) + const tools = makeTrustGraphMcpToolkitLayerFromConfig(config) return Layer.mergeAll( TrustGraphMcpHttpApiRoutes, @@ -1744,6 +1742,27 @@ const makeTrustGraphMcpHttpLayerFromConfig = ( ) } +const makeTrustGraphMcpToolkitLayerFromConfig = ( + config: TrustGraphMcpConfigShape, +) => + McpServer.toolkit(TrustGraphMcpToolkit).pipe( + Layer.provide(TrustGraphMcpToolkitLive), + Layer.provide(makeOpenAiProviderLayerFromConfig(config)), + ) + +const makeTrustGraphMcpStdioLayerFromConfig = ( + config: TrustGraphMcpConfigShape, +) => + makeTrustGraphMcpToolkitLayerFromConfig(config).pipe( + Layer.provide(McpServer.layerStdio({ + name: config.name, + version: config.version, + })), + Layer.provide(NodeStdio.layer), + Layer.provide(TrustGraphSocket.layer), + Layer.provide(Layer.succeed(TrustGraphMcpConfig, TrustGraphMcpConfig.of(config))), + ) + export const makeTrustGraphMcpHttpServerLayer = ( options: TrustGraphMcpOptions = {}, ) => @@ -1766,6 +1785,19 @@ export const makeTrustGraphMcpHttpLayer = ( ), ) +export const makeTrustGraphMcpStdioLayer = ( + options: TrustGraphMcpOptions = {}, +) => + Layer.unwrap( + loadTrustGraphMcpConfig(options).pipe( + Effect.map(makeTrustGraphMcpStdioLayerFromConfig), + ), + ) + export const runHttp = (options: TrustGraphMcpOptions = {}): void => { Layer.launch(makeTrustGraphMcpHttpServerLayer(options)).pipe(BunRuntime.runMain) } + +export const runStdio = (options: TrustGraphMcpOptions = {}): void => { + Layer.launch(makeTrustGraphMcpStdioLayer(options)).pipe(NodeRuntime.runMain) +}