-
Notifications
You must be signed in to change notification settings - Fork 103
[Client] Feat: Implement MCP client component #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[Client] Feat: Implement MCP client component #192
Conversation
Add Client, Builder, ClientProtocol, ClientSession, and transport implementations for communicating with MCP servers. Supports tools, resources, prompts, and real-time progress/logging notifications.
…g server requirements.
Co-authored-by: Christopher Hertel <[email protected]>
|
i like this very much and can follow your reason about opting into features unlike the server side 👍
|
| php -S localhost:8080 examples/http-discovery-userprofile/server.php | ||
|
|
||
| # Then run the client | ||
| php examples/client/http_discovery_userprofile.php |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| php examples/client/http_discovery_userprofile.php | |
| php examples/client/http_discovery_calculator.php |
|
|
||
| ```bash | ||
| # First, start an HTTP server | ||
| php -S localhost:8080 examples/http-discovery-userprofile/server.php |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
client example expects port 8000 and server example moved
| php -S localhost:8080 examples/http-discovery-userprofile/server.php | |
| php -S localhost:8000 examples/server/discovery-calculator/server.php |
not sure which one you wanted:
| php -S localhost:8080 examples/http-discovery-userprofile/server.php | |
| php -S localhost:8000 examples/server/discovery-userprofile/server.php |
This PR introduces an MCP client component for the SDK, enabling PHP applications to connect to MCP servers (via STDIO subprocess or HTTP) and interact with server-exposed tools, resources, and prompts.
Important
This PR is currently in draft mode to gather early feedback on the API design and architecture before finalizing. The core client functionality is working and I'm opening this early to ensure alignment with the project's direction before solidifying specific implementation decisions.
Motivation & Context
This addresses several community requests for client-side functionality (#185 and #15). The MCP ecosystem requires both servers (exposing tools, resources, prompts) and clients (consuming them). While the SDK has robust server support, this PR adds the complementary client-side implementation.
What's Changed
Core Client Components
Client— High-level API for connecting to servers, initializing sessions, calling tools, listing resources/prompts, and handling real-time notificationsBuilder— Fluent builder pattern for constructingClientinstances with timeouts, capabilities, handlers, and loggerProtocol— Central message dispatcher handling JSON-RPC request/response routing, server notifications, and server-initiated requests (sampling)Configuration— Value object holding client settings (timeouts, capabilities, retries)Transports
StdioClientTransport— Spawns a subprocess and communicates via stdin/stdout (ideal for local MCP servers)HttpClientTransport— Connects to HTTP-based MCP servers with SSE streaming support.ClientTransportInterface— Contract for custom transport implementationsHandler Registration Approach
Unlike the server, which auto-registers internal handlers for all standard notifications/requests (allowing user overrides with precedence), the client takes a more explicit opt-in approach:
ProgressNotificationHandleris internal — it stores progress data so the transport's tick loop can pull and execute user callbacksLoggingNotificationHandlerandSamplingRequestHandlermust be explicitly registered by the userThis is intentional as logging and sampling are capabilities the client not only chooses to support, but has to provide a mechanism on how to handle them, so handlers are registered when needed. To reduce boilerplate, I created convenience handlers that accept typed callbacks:
Regardless, users can still register their own custom handlers as they choose just like the server component
Session Management
ClientSession— In-memory session tracking pending requests, responses, and progress dataClientSessionInterface— Contract for session storageExceptions
TimeoutException— Thrown when requests exceed configured timeoutRequestException— Thrown when server returns an error responseConnectionException— Thrown when transport connection failsBug Fixes
CreateSamplingMessageRequest::fromParams()to properly hydrate raw JSON arrays into SamplingMessage objectsExamples Reorganization
examples/server/examples/client/:stdio_client_communication.php— Demo with logging, progress, and samplinghttp_client_communication.php— Same demo over HTTP with SSE streamingstdio_discovery_calculator.php— Tool discovery examplehttp_discovery_calculator.php— Same over HTTPWhy client examples are separated by transport: Unlike server examples (which auto-detect transport based on runtime context), client examples are inherently transport-specific — STDIO requires specifying the command and arguments to spawn, while HTTP requires an endpoint URL. Keeping them separate makes the examples cleaner and easier to follow (for now at least)
Request for Comments
I'd appreciate feedback on a few design decisions:
1. Transport Naming Convention
Currently:
StdioTransport,StreamableHttpTransport(namespaceMcp\Server\Transport)StdioClientTransport,HttpClientTransport(namespaceMcp\Client\Transport)Options:
StdioClientTransportandStdioServerTransportvsStdioTransport) — explicit but verboseStdioTransport), differentiated only by namespace — cleaner but requires careful importsWhat naming convention would be preferred?
2. Handler Interface Sharing
I originally intended to have shared handler interfaces at
Mcp\Handler(seesrc/Handler/RequestHandlerInterface.phpandsrc/Handler/NotificationHandlerInterface.php) that both client and server would use. The client uses them, but server handlers need aSessionInterfaceparameter that the shared interface doesn't include.What's the best approach here?
handle($request, $session), client uses interface withhandle($request)but moved tosrc/Client/Handler. Two differentRequestHandlerInterfacedefinitions.HandlerContextobject that wraps optional parameters (session, etc.), or a simplearray $context. Both sides use handle($request, $context) where context contents differ between client/server (I'm leaning towards this, but with an array context, though it'll be a breaking change)3. STDIO Implementation:
proc_openvssymfony/processStdioClientTransportuses nativeproc_open()with pipes for subprocess management. I was tempted to pull insymfony/processas a more mature solution, but since the server'sStdioTransportalso uses native functions, I kept it consistent.Should we consider adopting
symfony/processfor more robust process handling esp. for weird cases (Windows and co.), or stick with native functions to minimize dependencies?4. ClientSession Role
ClientSessioncurrently acts as in-memory storage for:Unlike server sessions, it's not persisted. It exists only for the lifetime of a connection, which works well. Should this remain a simple in-memory store, or is there a use case for persistent client sessions?
Looking forward to feedback on the overall direction. Happy to adjust the API design based on maintainer preferences before removing the draft status.