Skip to main content
Glama
effect-mcp-notebook.src.md7.69 kB
<!-- srcbook:{"language":"typescript"} --> # Effect for MCP: Hands‑On Srcbook Notebook This Srcbook gives you a runnable, minimal toolkit to build robust MCP tools with Effect‑TS, plus quick hooks into Srcbook resources and external docs. ## Learning objectives - Understand `Effect<A, E, R>` and how it maps to MCP tool handlers - Define a typed error algebra and map it to MCP error codes - Apply resiliency (retry + timeout) declaratively - Convert existing Promise code to Effect cleanly - Practice with short exercises and verify outputs in cells ## Big picture (A/E/R) at a glance ```mermaid flowchart TD A[Input args] --> V[Validation Effect] V -->|ok| T[Tool Logic Effect<A,E,R>] V -->|fail: ValidationError| M1[Map → MCP InvalidParams] T -->|retry/timeout wrapper| R[Resiliency] R -->|ok| O[Success A → content[]] R -->|fail: E| M2[Map typed E → MCP error] ``` Tip: In MCP, the external request/response shape does not change. Only your internals become typed and resilient. ###### package.json ```json { "type": "module", "dependencies": { "effect": "^3.5.0" } } ``` ## 1) Typed Errors (E) for MCP tools ###### errors.ts ```ts export class ValidationError { readonly _tag = 'ValidationError'; constructor(readonly message: string) {} } export class TimeoutError { readonly _tag = 'TimeoutError'; constructor(readonly ms?: number) {} } export class RateLimitError { readonly _tag = 'RateLimitError'; constructor(readonly retryAfterMs?: number) {} } export class AuthenticationError { readonly _tag = 'AuthenticationError'; constructor(readonly message = 'Auth failed') {} } export class ToolUnavailableError { readonly _tag = 'ToolUnavailableError'; constructor(readonly message = 'Tool unavailable') {} } export class UnexpectedError { readonly _tag = 'UnexpectedError'; constructor(readonly cause: unknown) {} } export type ToolError = | ValidationError | TimeoutError | RateLimitError | AuthenticationError | ToolUnavailableError | UnexpectedError; ``` ## 2) Resiliency wrapper (retry + timeout) ###### resiliency.ts ```ts import { Effect, Schedule, Duration } from 'effect' import type { ToolError } from './errors.ts' export interface ResiliencyPolicy { timeout: Duration.DurationInput retrySchedule: Schedule.Schedule<any, any, any> } export const defaultPolicy: ResiliencyPolicy = { timeout: Duration.seconds(5), retrySchedule: Schedule.exponential(Duration.millis(100)).pipe( Schedule.jittered, Schedule.recurs(2), ), } export const withResiliency = (key: string, p: ResiliencyPolicy) => <A, E extends ToolError, R>(eff: Effect.Effect<A, E, R>) => eff.pipe( Effect.retry(p.retrySchedule), Effect.timeout(p.timeout), ) ``` Try it: change `recurs(2)` to `recurs(5)` and re-run the tool in section 4 to see more retries in action (add transient failures to observe behavior). ## 3) MCP error mapping (typed → protocol) ###### mcp-mapping.ts ```ts import type { ToolError } from './errors.ts' // Minimal mapping without importing @modelcontextprotocol/sdk types export type McpErrorLike = { code: string; message: string } export function toMcp(error: ToolError): McpErrorLike { switch (error._tag) { case 'ValidationError': return { code: 'InvalidParams', message: error.message } case 'TimeoutError': return { code: 'RequestTimeout', message: 'Timed out' } case 'RateLimitError': return { code: 'RateLimited', message: 'Rate limited' } case 'AuthenticationError': return { code: 'Unauthorized', message: error.message } case 'ToolUnavailableError': return { code: 'ServiceUnavailable', message: error.message } default: return { code: 'InternalError', message: 'Unexpected error' } } } ``` ## 4) Minimal tool effect and runner ###### tool.ts ```ts import { Effect } from 'effect' import { ValidationError, type ToolError } from './errors.ts' import { withResiliency, defaultPolicy } from './resiliency.ts' import { toMcp } from './mcp-mapping.ts' type Result = { content: Array<{ type: 'text'; text: string }> } // Example: validate input and return a simple result function runTool(input: unknown): Effect.Effect<Result, ToolError> { return Effect.gen(function* (_) { if (!input || typeof (input as any).message !== 'string') { // Typed failure instead of throw return yield* _(Effect.fail(new ValidationError('"message" (string) is required'))) } const text = (input as any).message as string return { content: [{ type: 'text', text }] } }) } // Wrap with resiliency and run const runnable = withResiliency('demo.tool', defaultPolicy)(runTool({ message: 'hello from Effect' })) try { const out = await Effect.runPromise(runnable) console.log('OK', out) } catch (e) { const mcp = toMcp(e as ToolError) console.log('MCP ERROR', mcp) } ``` Exercise: make `runTool` randomly fail the first time with a `TimeoutError` to watch the retry logic kick in. Hint: keep a module‑level counter and `Effect.fail(new TimeoutError(…))` on the first call. ## 5) Pull docs from Srcbook repo (example) ###### fetch-srcbook-readme.ts ```ts // Node 18+ has global fetch const url = 'https://raw.githubusercontent.com/srcbookdev/srcbook/main/README.md' const res = await fetch(url) if (!res.ok) throw new Error('Failed to fetch Srcbook README') const text = await res.text() console.log(text.slice(0, 400) + '\n...') ``` ## 6) Context7 docs (via MCP) – how to query Use your Context7 MCP server to fetch library docs and feed them into tool logic or markdown cells. ```bash # Resolve a library id and fetch docs (run from your MCP client) mcp_context7-mcp_resolve-library-id "effect" mcp_context7-mcp_get-library-docs "/effect-ts/effect" --topic hooks ``` Then paste the returned snippets into a markdown cell or use them to generate code cells. ## 7) Primer (reference) This notebook follows the approach in `specs/effect-for-mcp-primer.md`: - Model tools as `Effect<A,E,R>` - Typed error algebra with deterministic MCP mapping - Declarative resiliency wrappers (retry + timeout) - Minimal environment; expand only as needed Export this Srcbook to share the pattern with your team. --- ## Bonus: Promise → Effect conversions (practice) ###### promise-to-effect.ts ```ts // Given: an async function that may reject with unknown async function fetchNumber(): Promise<number> { if (Math.random() < 0.5) throw new Error('flaky') return 42 } // Convert to Effect with a typed error import { Effect } from 'effect' import { UnexpectedError } from './errors.ts' const fetchNumberEffect = Effect.tryPromise({ try: () => fetchNumber(), catch: (cause) => new UnexpectedError(cause), }) try { const n = await Effect.runPromise(fetchNumberEffect) console.log('OK number =', n) } catch (e) { console.log('Typed error tag =', (e as any)._tag) } ``` ## Exercises 1) Add a new error type `DependencyError` and map it to MCP `ServiceUnavailable`. Modify `runTool` to fail with `DependencyError` when `process.env.SHOULD_FAIL === 'dep'`. 2) Extend `withResiliency` to accept an optional `onRetry` callback and log attempts. 3) Create a tiny validator that uses a Zod schema; on failure, produce `ValidationError` with the first issue message. Use the console output from each code cell to validate your work. ## Common pitfalls (and fixes) - Catch‑all without mapping: Always convert unknown failures into a typed `UnexpectedError` and then to MCP. - Throwing in `gen`: Prefer `Effect.fail(new TypedError(...))` so the type system tracks `E`. - Over‑retries: Cap retries with `recurs(n)` or a maximum elapsed constraint; add jitter to avoid thundering herds.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/waldzellai/clearthought-onepointfive'

If you have feedback or need assistance with the MCP directory API, please join our Discord server