General, streaming-friendly MCP (Model Context Protocol) client for Node.js.
- Spawns an MCP-compatible server as a child process
- Speaks JSON-RPC 2.0 over stdin / stdout
- Ignores fragile
Content-Lengthheaders and uses a robust JSON object scanner - Supports multiple requests per process (piped line-by-line)
- CLI + programmatic TypeScript API
This package is designed to be a generic, open-source MCP client. It works with any MCP-compliant server implementation, including the reference mcp-server package.
- Child process orchestration – monitors stderr, exit, error
- Handshake detection – first valid JSON object = handshake
- Framing-agnostic parsing – safely ignores
Content-Length - JSON-RPC 2.0 support – id-based pending map, timeouts
- CLI & Library – usable from terminal or TypeScript
npm install -g mcp-client
# or
npm install mcp-client --save-devmcp run "node dist/server.js"echo '{"jsonrpc":"2.0","id":1,"method":"providers.list"}' \
| mcp run "node ../mcp-server/dist/server.js"{
"jsonrpc": "2.0",
"id": 1,
"result": {
"providers": [
{
"provider": "openai",
"model": "gpt-4o-mini"
}
]
}
}printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"providers.list"}' \
'{"jsonrpc":"2.0","id":2,"method":"steps.list"}' \
| mcp run "node ../mcp-server/dist/server.js"{
"jsonrpc": "2.0",
"id": 1,
"result": { "providers": [ /* ... */ ] }
}
{
"jsonrpc": "2.0",
"id": 2,
"result": { "steps": [ /* ... */ ] }
}echo '{"jsonrpc":"2.0","id":3,"method":"scoring.schema"}' \
| mcp run "node ../mcp-server/dist/server.js"{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Method not found: scoring.schema"
}
}import { MCPProcess } from "mcp-client";
import type { JSONRPCRequest } from "mcp-client/jsonrpc";
async function main() {
const proc = new MCPProcess({
command: "node",
args: ["../mcp-server/dist/server.js"],
startupTimeoutMs: 4000,
shutdownTimeoutMs: 3000
});
proc.on("stderr", (msg) => process.stderr.write(String(msg)));
await proc.start();
const req: JSONRPCRequest = {
jsonrpc: "2.0",
id: 1,
method: "providers.list",
params: {}
};
const response = await proc.send(req);
console.log(JSON.stringify(response, null, 2));
await proc.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});The first valid JSON object received from stdout is treated as the handshake.
If the server delays or prints logs first, the client still proceeds safely.
Many servers emit:
Content-Length: 2888\r\n\r\n{ ... JSON ... }
But Content-Length is often wrong or mixed with logs.
This client instead:
- ignores Content-Length
- uses a streaming JSON scanner:
- finds
{ - tracks nested
{/} - handles JSON strings & escapes
- extracts full JSON frames
- finds
Works even with imperfect/experimental MCP servers.
- Requests stored in
Map<id, PendingEntry> - Responses resolve Promises
- Timeouts reject automatically
Enable verbose debugging:
MCP_DEBUG=1 mcp run "node dist/server.js"Shows:
- handshake detection
- scan events
- JSON parse errors
- child process exits
- forwarded stderr
- Server must output valid JSON frames
- First JSON object is always treated as handshake
- JSON-like logs printed before handshake may be misinterpreted
MIT – see LICENSE.