Skip to content

Repository files navigation

@ingram-tech/mcp-client

A stateless MCP client: list a remote server's tools and call one.

Each operation is a self-contained JSON-RPC POST over Streamable HTTP. There is no session and no initialize handshake to keep alive. Bun and Node 20+.

npm install @ingram-tech/mcp-client
import { createMcpClient } from "@ingram-tech/mcp-client";

const mcp = createMcpClient({ clientInfo: { name: "my-host", version: "1.0" } });

const { tools, protocolVersion } = await mcp.listTools("https://mcp.example.com/mcp", {
	headers: { Authorization: `Bearer ${token}` },
});

const text = await mcp.callTool(
	"https://mcp.example.com/mcp",
	"search",
	{ query: "invoices due this week" },
	{ headers: { Authorization: `Bearer ${token}` }, protocolVersion },
);

Tool results come back as text a model can read: structuredContent as JSON, otherwise the content blocks concatenated, with isError results prefixed error: so the model can recover.

What it speaks

  • MCP 2026-07-28, the stateless revision: the _meta triple on every request, Mcp-Method / Mcp-Name mirrored into headers, ttlMs on tools/list. A server that rejects those headers (400/404/405 on tools/list) is retried once as 2025-06-18 and reported as such in protocolVersion. Hand that back to callTool, which never probes, because a tool call is not safe to send twice.

  • Tasks (io.modelcontextprotocol/tasks): a server may answer tools/call with a task handle. The call polls it to completion at the server's pollIntervalMs and returns the final result; after taskWaitMs (default 5 min) it cancels the task and throws.

  • Input requests (resultType: "input_required"): a server may need an answer from the end user before it can finish. The call throws McpInputRequired carrying inputRequests and the opaque requestState (or taskId). Ask the user, then retry the same call with answers, or resumeTask for a task:

    try {
    	await mcp.callTool(url, name, args, opts);
    } catch (e) {
    	if (e instanceof McpInputRequired) {
    		const inputResponses = await askTheUser(e.inputRequests);
    		await (e.taskId
    			? mcp.resumeTask(url, { taskId: e.taskId, inputResponses }, opts)
    			: mcp.callTool(url, name, args, {
    					...opts,
    					answers: { inputResponses, requestState: e.requestState },
    				}));
    	}
    }
  • x-mcp-header: a server may mark a primitive property of a tool's inputSchema to be mirrored into an Mcp-Param-<Name> header, so its gateway can route on it without parsing the body. The mirror is applied on every call; a tool whose annotations break the spec's constraints is returned under dropped rather than called wrong.

  • Responses as application/json or as a text/event-stream carrying one JSON-RPC frame.

Resources, prompts, sampling and roots are not implemented.

Errors

Every failure is an MCPError. transport is true when the server never handled the call (a network failure or a non-2xx status), and false for an in-protocol JSON-RPC error, which may be the model misusing a healthy tool. Error text never echoes a response body.

Options

createMcpClient({
	clientInfo: { name: "my-host", version: "1.0" },
	timeoutMs: 30_000,
	taskWaitMs: 5 * 60_000,
	fetch: mySafeFetch,
});

Requests leave through @ingram-tech/safe-fetch by default: the URL is whatever the user configured, and a client that dereferences it from inside your network needs the SSRF guard. Pass fetch to replace it.

Wire constants

The protocol version strings, the _meta keys and the =?base64?…?= header encoding are exported so a server and a client built on this package cannot drift on them.

License

MIT

About

A stateless MCP client: list a remote server's tools and call one. Tasks, input requests, x-mcp-header.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages