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