Add Effect stdio MCP entrypoint

This commit is contained in:
elpresidank 2026-06-02 08:59:53 -05:00
parent 0fb10aca73
commit e311315556
5 changed files with 136 additions and 16 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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:*",

View file

@ -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));
});
});

View file

@ -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)
}