milpa/mcp-client

MCP (Model Context Protocol) client for the Milpa PHP framework: stdio and HTTP/SSE transports, JSON-RPC framing, capability negotiation, and tool/resource discovery for connecting to external MCP servers.

Maintainers

Package info

github.com/getmilpa/mcp-client

pkg:composer/milpa/mcp-client

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.0 2026-07-08 11:48 UTC

This package is auto-updated.

Last update: 2026-07-14 22:57:39 UTC


README

Milpa

Milpa MCP Client

An MCP (Model Context Protocol) client for PHP — stdio and HTTP/SSE transports, typed tool/resource contracts, and connection management for talking to external MCP servers. McpClientManager connects to several servers at once and routes tool calls to the right one by a namespaced registry name.

CI Packagist PHP License Docs

milpa/mcp-client is the outbound half of MCP for the Milpa framework: it lets a PHP process act as an MCP client, connecting to one or more external MCP servers — local ones spawned as subprocesses (stdio) or remote ones over HTTP with Server-Sent Events — and discovering, listing, and calling the tools and resources they expose. No product coupling: HttpSseTransport speaks HTTP through PSR-18 (Psr\Http\Client\ClientInterface), defaulting to guzzlehttp/guzzle when no client is injected.

Install

composer require milpa/mcp-client

Quick example

Register a server, connect, and call one of its tools. McpClientManager picks the right TransportInterface from the transport key in the config you pass it:

use Milpa\McpClient\McpClientManager;

$manager = new McpClientManager();

$manager->registerServer('calculator', [
    'transport' => 'stdio',
    'command' => 'php',
    'args' => [__DIR__ . '/mcp-servers/calculator.php'],
]);

$manager->connect('calculator');

connect() runs the MCP handshake (initializenotifications/initialized) and discovers the server's tools and resources; every tool is indexed under a namespaced registry namemcp_{server}_{tool} — so a McpClientManager juggling several servers never collides on a bare tool name:

foreach ($manager->listAllTools() as $tool) {
    echo $tool->getRegistryName(), '', $tool->description, "\n";
}
// mcp_calculator_add — Add two numbers

$result = $manager->callTool('mcp_calculator_add', ['a' => 2, 'b' => 3]);
// ['sum' => 5]

$manager->disconnectAll();

A remote server over HTTP/SSE looks the same, just with a different transport config:

$manager->registerServer('remote-docs', [
    'transport' => 'http-sse',
    'url' => 'https://mcp.example.com/',
    'headers' => ['Authorization' => 'Bearer ' . $token],
    'timeout' => 30,
]);

connectAll() connects every registered server and returns a [name => Throwable] map of the ones that failed, instead of throwing on the first bad server.

Bringing your own HTTP client (PSR-18)

McpClientManager::registerServer() always builds its own HttpSseTransport from the config array, so it always gets the default Guzzle client. To inject your own PSR-18 client — a shared connection pool, a client wrapped with retry/circuit-breaker middleware, or a test double — construct HttpSseTransport directly and wrap it in a McpConnection yourself:

use Milpa\McpClient\McpConnection;
use Milpa\McpClient\Transports\HttpSseTransport;

$transport = new HttpSseTransport(
    baseUrl: 'https://mcp.example.com/',
    headers: ['Authorization' => 'Bearer ' . $token],
    serverName: 'remote-docs',
    httpClient: $yourPsr18Client,     // Psr\Http\Client\ClientInterface; omit for Guzzle
    requestFactory: $yourPsr17Factory, // Psr\Http\Message\RequestFactoryInterface; optional
    streamFactory: $yourPsr17Factory,  // Psr\Http\Message\StreamFactoryInterface; optional
    logger: $yourLogger,               // Psr\Log\LoggerInterface; optional, see below
);

$connection = new McpConnection('remote-docs', $transport);
$connection->connect();

$timeout only configures the default Guzzle client — it has no effect once you inject your own $httpClient, since PSR-18's sendRequest() takes no per-call options.

notify() (fire-and-forget JSON-RPC notifications) never throws — that's the TransportInterface contract — but a transport failure during notify() is reported to $logger (at warning level) instead of being silently dropped, when a logger is injected.

What it is

  • McpClientManager — the entry point most callers use. Registers servers from plain config arrays, owns their McpConnections, and aggregates + routes tool calls across all of them by registry name.
  • McpConnection — one server's live session: handshake, capability discovery, listTools() / getTool() / callTool(), listResources() / readResource(), and refresh() to re-discover after the server's tool list changes.
  • TransportInterface — the seam a transport implements: connect(), disconnect(), isConnected(), request() (JSON-RPC call, blocks for a reply), and notify() (JSON-RPC notification, no reply expected).
    • StdioTransport — spawns the server as a subprocess (proc_open) and speaks newline-delimited JSON-RPC over its stdin/stdout.
    • HttpSseTransport — POSTs JSON-RPC through an injectable PSR-18 ClientInterface (Guzzle by default) and parses the reply whether the server answers with a plain JSON body or a text/event-stream SSE payload.
  • Typed contractsMcpTool, McpResource, and McpCapabilities are immutable value objects built with fromArray() from the server's raw JSON-RPC responses, not arrays passed around by convention.
  • A layered exception hierarchy — every exception extends McpException, which carries the offending serverName for context; McpConnectionException, McpTransportException, McpToolException, and McpServerException (which also carries the JSON-RPC errorCode and errorData) narrow the failure mode.

Be honest about scope: this package is a client only — it does not implement or host an MCP server. It does not persist tool schemas, cache results, or retry failed calls; those policies belong to the host application.

What's inside

Namespace What it provides
Milpa\McpClient McpClientManager, McpConnection
Milpa\McpClient\Contracts TransportInterface, McpTool, McpResource, McpCapabilities, McpException, McpConnectionException, McpServerException, McpToolException, McpTransportException
Milpa\McpClient\Transports StdioTransport, HttpSseTransport

Every public symbol carries a DocBlock.

Requirements

  • PHP ≥ 8.3
  • guzzlehttp/guzzle ^7.10 — the default PSR-18 implementation HttpSseTransport falls back to when no ClientInterface is injected (also brings guzzlehttp/psr7, used as the default PSR-17 factory)
  • psr/http-client, psr/http-factory, psr/http-message, psr/log — the interfaces HttpSseTransport's constructor is typed against

Documentation

Full API reference: getmilpa.github.io/mcp-client — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing

Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.

License

Apache-2.0 © Rodrigo Vicente - TeamX Agency.

Milpa is designed, built, and maintained by Rodrigo Vicente - TeamX Agency.