Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

McpTest unstable

In-memory test client for McpServer registrations.

McpTest.makeClient runs the MCP server over an in-memory RpcServer.Protocol, performs the handshake for the selected protocol version, and returns a typed client. Requests travel through the same JSON-RPC encoding, routing, middleware, and response encoding as the stdio and HTTP transports, so tests reach protocol encoding, list methods, sessions, and notifications without opening a transport.

2 exports Since v4.0.0 Source

Constructors

makeClient

unstable
Since v4.0.0

Creates an in-memory MCP client for a registration layer.

Details

The server is built from McpServer.layer with the supplied protocols and registrations, over an in-memory RpcServer.Protocol. The client speaks options.protocolVersion, by default the first adapter's version: stateful versions initialize and send notifications/initialized before makeClient returns; the stateless version attaches request metadata to every request instead.

Because requests reach handlers through the server runner, the transport built-ins are discharged the same way as on stdio: McpRequestContext on every request, and McpServerClient only when every adapter in protocols is stateful (see McpServer.ProvidedServices). Any other McpRequest marker in registrations stays in the requirements and must be met by MCP middleware; providing the service directly does not discharge it.

The client and server live in the surrounding Scope.

See

Signature

declare const makeClient: <Protocols extends Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>, RA = never, RE = never, RR = never>(registrations: Layer.Layer<RA, RE, RR>, options: {
readonly clientCapabilities?: McpSchema.ClientCapabilities;
readonly clientInfo?: McpSchema.Implementation;
readonly extensions?: NonNullable<McpSchema.ServerCapabilities["extensions"]>;
readonly instructions?: string;
readonly name?: string;
readonly protocols: Protocols;
readonly protocolVersion?: Protocols[number]["protocolVersion"];
readonly version?: string;
}) => Effect.Effect<Client, Cause.IllegalArgumentError | RE, Scope.Scope | Exclude<McpServer.McpRequest.Without<RR>, McpServer.McpServer> | McpServer.McpRequest.From<"Requires", Exclude<McpServer.McpRequest.Only<"Requires", RR>, McpServer.ProvidedServices<Protocols>>>>

Example Reading a resource in a test

import { Effect } from "effect"
import { McpProtocol, McpServer, McpTest } from "effect/ai"
const registrations = McpServer.resource({
uri: "test://greeting",
name: "greeting",
content: Effect.succeed("hello")
})
const program = Effect.gen(function*() {
const client = yield* McpTest.makeClient(registrations, {
protocols: [McpProtocol.v2025_11_25]
})
const result = yield* client.readResource({ uri: "test://greeting" })
return result.contents.map((content) => content.uri)
}).pipe(Effect.scoped)
const actual = await Effect.runPromise(program)
actual // => ["test://greeting"]

Models

Client

interface unstable
Since v4.0.0

A typed in-memory MCP client connected to a running McpServer.

Details

Each request method mirrors one entry of McpSchema.ClientRequestRpcs, except initialize, which makeClient sends during the handshake. Requests are encoded with the client schemas and decoded with the server's selected protocol adapter, so results and errors match what a wire client observes.

Server-to-client notifications are queued on notifications in their wire encoding, in delivery order. Server-to-client requests (elicitation, sampling, roots) are rejected with a method-not-found error.

Signature

interface Client {
readonly callTool: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly arguments?: {
[key: string]: any;
};
readonly name: string;
}) => Effect<CallToolResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly complete: (request: {
readonly argument: {
readonly name: string;
readonly value: string;
};
readonly context?: {
readonly arguments?: {
[key: string]: string;
};
};
readonly ref: ResourceReference | PromptReference;
}) => Effect<CompleteResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly getPrompt: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly arguments?: {
[key: string]: string;
};
readonly name: string;
readonly title?: string;
}) => Effect<GetPromptResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly listPrompts: (request?: PaginatedRequestMeta) => Effect<ListPromptsResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly listResources: (request?: PaginatedRequestMeta) => Effect<ListResourcesResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly listResourceTemplates: (request?: PaginatedRequestMeta) => Effect<ListResourceTemplatesResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly listTools: (request?: PaginatedRequestMeta) => Effect<ListToolsResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly notifications: Dequeue<{
readonly _tag: "Notification";
readonly method: "notifications/cancelled";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
readonly reason?: string;
readonly requestId: string | number;
};
} | {
readonly _tag: "Notification";
readonly method: "notifications/progress";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
readonly message?: string;
readonly progress: number;
readonly progressToken: string | number;
readonly total?: number;
};
} | {
readonly _tag: "Notification";
readonly method: "notifications/resources/list_changed";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined;
} | {
readonly _tag: "Notification";
readonly method: "notifications/resources/updated";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
readonly uri: string;
};
} | {
readonly _tag: "Notification";
readonly method: "notifications/prompts/list_changed";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined;
} | {
readonly _tag: "Notification";
readonly method: "notifications/tools/list_changed";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined;
} | {
readonly _tag: "Notification";
readonly method: "notifications/message";
readonly payload: {
readonly _meta?: {
[key: string]: unknown;
};
readonly data: any;
readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
readonly logger?: string;
};
} | {
readonly _tag: "Notification";
readonly method: "notifications/elicitation/complete";
readonly payload: {
readonly elicitationId: string;
};
}>;
readonly ping: Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly protocolVersion: ProtocolVersion;
readonly readResource: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly uri: string;
}) => Effect<ReadResourceResult, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly server: {
readonly addPrompt: (options: {
readonly annotations: Context<never>;
readonly completions: Record<string, (input: string, context: CompletionContext) => Effect.Effect<CompleteResult, InternalError, McpRequestContext>>;
readonly handle: (params: Record<string, string>) => Effect<GetPromptResult | InputRequired, InvalidParams | InternalError, McpRequestContext>;
readonly prompt: Prompt;
readonly services?: Context<never>;
}) => Effect<void>;
readonly addResource: (options: {
readonly annotations: Context<never>;
readonly handle: Effect<ReadResourceResult, InternalError, McpRequestContext>;
readonly resource: Resource;
readonly services?: Context<never>;
}) => Effect<void>;
readonly addResourceTemplate: (options: {
readonly annotations: Context<never>;
readonly completions: Record<string, (input: string, context: CompletionContext) => Effect.Effect<CompleteResult, InternalError>>;
readonly handle: (uri: string, params: Array<string>) => Effect<ReadResourceResult, InvalidParams | InternalError, McpRequestContext>;
readonly routerPath: string;
readonly services?: Context<never>;
readonly template: ResourceTemplate;
}) => Effect<void>;
readonly addTool: (options: {
readonly annotations: Context<never>;
readonly handle: (payload: any) => Effect<CallToolResult | InputRequired, InvalidParams | InternalError, McpRequestContext>;
readonly services?: Context<never>;
readonly tool: Tool;
}) => Effect<void>;
readonly callTool: (requests: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly arguments?: {
[key: string]: any;
};
readonly name: string;
}) => Effect<CallToolResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>;
readonly completion: (complete: {
readonly argument: {
readonly name: string;
readonly value: string;
};
readonly context?: {
readonly arguments?: {
[key: string]: string;
};
};
readonly ref: ResourceReference | PromptReference;
}) => Effect<CompleteResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>;
readonly findResource: (uri: string) => Effect<ReadResourceResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>;
readonly getPromptResult: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly arguments?: {
[key: string]: string;
};
readonly name: string;
readonly title?: string;
}) => Effect<GetPromptResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>;
readonly notifications: {
"notifications/cancelled": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
readonly reason?: string;
readonly requestId: string | number;
}, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/message": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
readonly data: any;
readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
readonly logger?: string;
}, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/progress": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
readonly message?: string;
readonly progress: number;
readonly progressToken: string | number;
readonly total?: number;
}, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/prompts/list_changed": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/resources/list_changed": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/resources/updated": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
readonly uri: string;
}, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
"notifications/tools/list_changed": <AsQueue extends boolean = false, Discard = false>(input: {
readonly _meta?: {
[key: string]: unknown;
};
} | undefined, options?: {
readonly context?: Context<never>;
readonly discard?: Discard;
readonly headers?: Input;
}) => Effect<Discard extends true ? void : void, Discard extends true ? never : never, never>;
};
readonly notifyElicitationComplete: (options: {
readonly clientId: number;
readonly elicitationId: string;
}) => Effect<void>;
readonly prompts: readonly Array<{
readonly annotations: Context<never>;
readonly prompt: Prompt;
}>;
readonly resources: readonly Array<{
readonly annotations: Context<never>;
readonly resource: Resource;
}>;
readonly resourceTemplates: readonly Array<{
readonly annotations: Context<never>;
readonly template: ResourceTemplate;
}>;
readonly tools: readonly Array<{
readonly annotations: Context<never>;
readonly tool: Tool;
}>;
};
readonly setLevel: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly level: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
}) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly subscribe: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly uri: string;
}) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
readonly unsubscribe: (request: {
readonly _meta?: {
[key: string]: unknown;
readonly progressToken?: string | number;
};
readonly uri: string;
}) => Effect<void, ParseError | McpErrorBase | InvalidRequest | MethodNotFound | InvalidParams | InternalError>;
}