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-clientimport { 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.
-
MCP 2026-07-28, the stateless revision: the
_metatriple on every request,Mcp-Method/Mcp-Namemirrored into headers,ttlMsontools/list. A server that rejects those headers (400/404/405 ontools/list) is retried once as 2025-06-18 and reported as such inprotocolVersion. Hand that back tocallTool, which never probes, because a tool call is not safe to send twice. -
Tasks (
io.modelcontextprotocol/tasks): a server may answertools/callwith a task handle. The call polls it to completion at the server'spollIntervalMsand returns the final result; aftertaskWaitMs(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 throwsMcpInputRequiredcarryinginputRequestsand the opaquerequestState(ortaskId). Ask the user, then retry the same call withanswers, orresumeTaskfor 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'sinputSchemato be mirrored into anMcp-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 underdroppedrather than called wrong. -
Responses as
application/jsonor as atext/event-streamcarrying one JSON-RPC frame.
Resources, prompts, sampling and roots are not implemented.
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.
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.
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.
MIT