← All 41 libraries

SugarMcp

SugarMcp

MCP client core — JSON-RPC codec, stdio transport, tool narrowing

component-package mcp json-rpc

SugarMcp code coverage

A Model Context Protocol client core the whole ecosystem can share: JSON-RPC 2.0 envelopes, a newline-framed stdio transport with a bounded, orphan-proof child lifecycle, the initialize/tools handshake, and deny-before-allow tool narrowing. Extracted from sugar-crush's src/MCP stack and McpMessage codec, so every surface that talks to an MCP server rides the same framing, the same timeouts, and the same error shapes. First-party — MCP is a spec, not a Go port.

Install

composer require sugarcraft/sugar-mcp:@dev

Quickstart

use SugarCraft\Mcp\StdioMcpServer;

$server = new StdioMcpServer(
    name: 'docs',
    command: 'npx',
    args: ['-y', '@modelcontextprotocol/server-fetch'],
);

$server->start();              // initialize → initialized → tools/list, one 60s clock
foreach ($server->listTools() as $tool) {
    echo $tool->name, ' — ', $tool->description, PHP_EOL;
}

$result = $server->callTool('fetch', ['url' => 'https://example.com']);
// ['content' => [['type' => 'text', 'text' => '…']]]  (scalars wrapped at the boundary)

$server->stop();               // close pipes first, then TERM→KILL→reap. Never an orphan.
// __destruct calls stop() for you if you forget.

The model

McpMessageJSON-RPC 2.0 envelope: parse() / request() / notification() / success() / error(); a resultSet sentinel keeps explicit "result": null representable; toJson() is the wire and fails closed on unencodable payloads, toArray() is inspection.
McpTool / McpServerOne tool definition value object with a well-typedness gate that refuses malformed server output, plus the start/stop/listTools/callTool interface.
StdioMcpServerargv-array proc_open child, newline-delimited framing, 64 KiB rolling stderr tail absorbed on every wait set, one wall clock across the whole handshake.
ExchangeLockSerialises whole request/response exchanges when forked processes share one stdio connection; records a W/R phase so the next holder resynchronises after a killed one, and reports failed state writes.
McpRouterDeny-patterns-then-allowlist narrowing over any set of McpServer instances; fnmatch globs; empty allowlist means allow-all; malformed entries fail loud.

Use it for

Source

Try the quickstart →

API

ClassMethodDescription
McpMessageparse(string $json): ?selfDecode a wire frame; null for anything that is not a valid JSON-RPC 2.0 message
McpMessagerequest(string $id, string $method, ?array $params) / notification(string $method, ?array $params): selfOutbound request and notification envelopes
McpMessagesuccess(string $id, mixed $result) / error(string $id, int $code, string $message, mixed $data = null): selfOutbound response envelopes; result is mixed by law
McpMessagetoJson(): string / isRequest() / isResponse() / isNotification() / isError(): boolWire serialization + role predicates; toJson() throws InvalidArgumentException for an envelope JSON cannot carry (INF/NAN, invalid UTF-8) instead of emitting a blank frame
StdioMcpServerstart(): void / stop(): void / isUp(): boolHandshake under one deadline; every leg must succeed — an initialize or tools/list error or missing reply, or an undelivered notifications/initialized, reaps the child and throws RuntimeException. Close-pipes-first teardown with TERM→KILL→reap via candy-core BoundedShutdown
StdioMcpServerlistTools(): list<McpTool> / callTool(string $name, array $arguments): arraytools/list result; tools/call with scalar-result wrapping at the boundary, no wall-clock deadline but a liveness probe on idle polls (a dead server ends the call within about a second, even when a forked helper holds its pipes); an unencodable argument becomes an {"error": …} payload
StdioMcpServerrequest(string $method, ?array $params = null, ?float $deadline = null): ?McpMessage / notify(string $method, ?array $params = null, ?float $deadline = null): boolEscapeway to any other protocol method (resources, prompts) without subclasses; notify() reports whether the whole line went out
McpToolfromArray(array $def, string $serverName): self / tryFromArray(?array $def, string $serverName): ?selfParse a tool definition; tryFromArray gates well-typedness before construction
McpRouterresolveAllowedTools(array $allowList): array / resolveAllowedServers(array $allowList): list<string>Deny-first narrowing; empty allowlist = allow-all
McpRouterstatic serverAllowed(string $name, array $allowList): boolExact or fnmatch-glob membership; malformed entries throw
ExchangeLockstatic new(string $label, ?string $dir = null): self / static sweepStale(?string $dir = null): intCross-process flock for one stdio connection shared across pcntl_fork(); new() sweeps lock files whose owner died, then creates this owner's file. create() remains as a deprecated alias of new()
ExchangeLockacquire(?float $deadline, \Closure $serverAlive): bool / load(): array / store(string $phase, string $buffer): bool / markPhase(string $phase): boolPhase byte + carried read buffer under the lock; store()/markPhase() return false when the write did not land, and an empty file loads as dirty