Kick off the SDK based on Symfony's MCP SDK

This commit is contained in:
Christopher Hertel 2025-07-25 11:40:39 +02:00
commit ee0dffe369
No known key found for this signature in database
GPG key ID: 111F6BFF7B6B5AA3
89 changed files with 3924 additions and 0 deletions

6
.gitattributes vendored Normal file
View file

@ -0,0 +1,6 @@
/.git* export-ignore
/examples export-ignore
/tests export-ignore
/.php-cs-fixer.dist.php export-ignore
/phpstan.dist.neon export-ignore
/phpunit.xml.dist export-ignore

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @chr-hertel @Nyholm @CodeWithKyrian

64
.github/workflows/pipeline.yml vendored Normal file
View file

@ -0,0 +1,64 @@
name: pipeline
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
dependencies: ['lowest', 'highest']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: "none"
- name: Install Composer
uses: "ramsey/composer-install@v3"
with:
dependency-versions: "${{ matrix.dependencies }}"
- name: Composer Validation
run: composer validate --strict
- name: Install PHP Dependencies
run: composer install --no-scripts
- name: Tests
run: vendor/bin/phpunit
qa:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: "none"
- name: Install Composer
uses: "ramsey/composer-install@v3"
- name: Composer Validation
run: composer validate --strict
- name: Install PHP Dependencies
run: composer install --no-scripts
- name: Code Style PHP
run: vendor/bin/php-cs-fixer fix --dry-run
- name: PHPStan
run: vendor/bin/phpstan analyse

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.phpunit.cache
.php-cs-fixer.cache
composer.lock
vendor

45
.php-cs-fixer.dist.php Normal file
View file

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!file_exists(__DIR__.'/src')) {
exit(0);
}
$fileHeaderParts = [
<<<'EOF'
This file is part of the official PHP MCP SDK.
A collaboration between Symfony and the PHP Foundation.
EOF,
<<<'EOF'
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF,
];
return (new PhpCsFixer\Config())
// @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7777
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'protected_to_private' => false,
'declare_strict_types' => false,
'header_comment' => [
'header' => implode('', $fileHeaderParts),
],
'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
])
->setRiskyAllowed(true)
->setFinder((new PhpCsFixer\Finder())->in(__DIR__))
;

24
CHANGELOG.md Normal file
View file

@ -0,0 +1,24 @@
CHANGELOG
=========
0.1
---
* Add Model Context Protocol (MCP) implementation for LLM-application communication
* Add JSON-RPC based protocol handling with `JsonRpcHandler`
* Add three core MCP capabilities:
- Resources: File-like data readable by clients (API responses, file contents)
- Tools: Functions callable by LLMs (with user approval)
- Prompts: Pre-written templates for specific tasks
* Add multiple transport implementations:
- Symfony Console Transport for testing and CLI applications
- Stream Transport supporting Server-Sent Events (SSE) and HTTP streaming
- STDIO transport for command-line interfaces
* Add capability chains for organizing features:
- `ToolChain` for tool management
- `ResourceChain` for resource management
- `PromptChain` for prompt template management
* Add Server component managing transport connections
* Add request/notification handlers for MCP operations
* Add standardized interface enabling LLMs to interact with external systems
* Add support for building LLM "plugins" with extra context capabilities

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

25
Makefile Normal file
View file

@ -0,0 +1,25 @@
.PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest
deps-stable:
composer update --prefer-stable
deps-low:
composer update --prefer-lowest
cs:
vendor/bin/php-cs-fixer fix --diff --verbose
phpstan:
vendor/bin/phpstan --memory-limit=-1
tests:
vendor/bin/phpunit
coverage:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage
ci: ci-stable
ci-stable: deps-stable cs phpstan tests
ci-lowest: deps-low cs phpstan tests

46
composer.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "mcp/sdk",
"type": "library",
"description": "Model Context Protocol SDK for Client and Server applications in PHP",
"license": "MIT",
"authors": [
{
"name": "Christopher Hertel",
"email": "mail@christopher-hertel.de"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
},
{
"name": "Kyrian Obikwelu",
"email": "koshnawaza@gmail.com"
}
],
"require": {
"php": "^8.1",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/uid": "^6.4 || ^7.0"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.5",
"symfony/console": "^6.4 || ^7.0",
"psr/cache": "^3.0",
"php-cs-fixer/shim": "^3.84"
},
"suggest": {
"symfony/console": "To use SymfonyConsoleTransport for STDIO",
"psr/cache": "To use CachePoolStore with SSE Transport"
},
"autoload": {
"psr-4": {
"Mcp\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Mcp\\Tests\\": "tests/"
}
}
}

157
doc/index.rst Normal file
View file

@ -0,0 +1,157 @@
Model Context Protocol SDK
==========================
The PHP MCP SDK is the low level library that enables communication between a PHP application and an LLM model.
Installation
------------
Install the SDK using Composer:
.. code-block:: terminal
$ composer require mcp/sdk
Usage
-----
The `Model Context Protocol`_ is built on top of JSON-RPC. There two types of
messages. A Notification and Request. The Notification is just a status update
that something has happened. There is never a response to a Notification. A Request
is a message that expects a response. There are 3 concepts/capabilities that you
may use. These are::
1. **Resources**: File-like data that can be read by clients (like API responses or file contents)
1. **Tools**: Functions that can be called by the LLM (with user approval)
1. **Prompts**: Pre-written templates that help users accomplish specific tasks
The SDK comes with NotificationHandlers and RequestHandlers which are expected
to be wired up in your application.
JsonRpcHandler
..............
The ``Mcp\Server\JsonRpcHandler`` is the heart of the SDK. It is here
you inject the NotificationHandlers and RequestHandlers. It is recommended to use
the built-in handlers in ``Mcp\Server\NotificationHandlers\*`` and
``Mcp\Server\RequestHandlers\*``.
The ``Mcp\Server\JsonRpcHandler`` is started and kept running by
the ``Mcp\Server``
Transports
..........
The SDK supports multiple transports for sending and receiving messages. The
``Mcp\Server`` is using the transport to fetch a message, then
give it to the ``Mcp\Server\JsonRpcHandler`` and finally send the
response/error back to the transport. The SDK comes with a few transports::
1. **Symfony Console Transport**: Good for testing and for CLI applications
1. **Stream Transport**: It uses Server Side Events (SSE) and HTTP streaming
Capabilities
............
Any client would like to discover the capabilities of the server. Exactly what
the server supports is defined in the ``Mcp\Server\RequestHandler\InitializeHandler``.
When the client connects, it sees the capabilities and will ask the server to list
the tools/resource/prompts etc. When you want to add a new capability, example a
**Tool** that can tell the current time, you need to provide some metadata to the
``Mcp\Server\RequestHandler\ToolListHandler``::
namespace App;
use Mcp\Capability\Tool\MetadataInterface;
class CurrentTimeToolMetadata implements MetadataInterface
{
public function getName(): string
{
return 'Current time';
}
public function getDescription(): string
{
return 'Returns the current time in UTC';
}
public function getInputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'format' => [
'type' => 'string',
'description' => 'The format of the time, e.g. "Y-m-d H:i:s"',
'default' => 'Y-m-d H:i:s',
],
],
'required' => [],
];
}
}
We would also need a class to actually execute the tool::
namespace App;
use Mcp\Capability\Tool\IdentifierInterface;
use Mcp\Capability\Tool\ToolCall;
use Mcp\Capability\Tool\ToolCallResult;
use Mcp\Capability\Tool\ToolExecutorInterface;
class CurrentTimeToolExecutor implements ToolExecutorInterface, IdentifierInterface
{
public function getName(): string
{
return 'Current time';
}
public function call(ToolCall $input): ToolCallResult
{
$format = $input->arguments['format'] ?? 'Y-m-d H:i:s';
return new ToolCallResult(
(new \DateTime('now', new \DateTimeZone('UTC')))->format($format)
);
}
}
If you have multiple tools, you can put them in a ToolChain::
$tools = new ToolChain([
new CurrentTimeToolMetadata(),
new CurrentTimeToolExecutor(),
]);
$jsonRpcHandler = new Mcp\Server\JsonRpcHandler(
new Mcp\Message\Factory(),
[
new ToolCallHandler($tools),
new ToolListHandler($tools),
// Other RequestHandlers ...
],
[
// Other NotificationHandlers ...
],
new NullLogger()
);
With this metadata and executor, the client can now call the tool.
Extending the SDK
-----------------
If you want to extend the SDK, you can create your own RequestHandlers and NotificationHandlers.
The provided one are very good defaults for most applications but they are not
a requirement.
If you do decide to use them, you get the benefit of having a well-defined interfaces
and value objects to work with. They will assure that you follow the `Model Context Protocol`_.
specification.
You also have the Transport abstraction that allows you to create your own transport
if non of the standard ones fit your needs.
.. _`Model Context Protocol`: https://modelcontextprotocol.io/

29
examples/cli/README.md Normal file
View file

@ -0,0 +1,29 @@
# Example app with CLI
This is just for testing and debugging purposes.
Install and create symlink with:
```bash
cd /path/to/your/project/examples/cli
composer update
rm -rf vendor/mcp/sdk/src
ln -s /path/to/your/project/src /path/to/your/project/examples/cli/vendor/mcp/sdk/src
```
Run the CLI with:
```bash
DEBUG=1 php index.php
```
You will see debug outputs to help you understand what is happening.
In this terminal you can now test add some json strings. See `example-requests.json`.
Run with Inspector:
```bash
npx @modelcontextprotocol/inspector php index.php
```

View file

@ -0,0 +1,27 @@
{
"name": "mcp/cli-server-example",
"description": "An example application for CLI",
"license": "MIT",
"type": "project",
"authors": [
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
}
],
"require": {
"php": ">=8.1",
"mcp/sdk": "@dev",
"symfony/console": "^7.2"
},
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"repositories": [
{ "type": "path", "url": "../../" }
]
}

View file

@ -0,0 +1,12 @@
[
{"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": []},
{"jsonrpc": "2.0", "id": 2, "method": "resources/read", "params": {"uri": "file:///project/src/main.rs"}},
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time"}},
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "Current time","arguments": {"format": "Y-m-d"}}},
{"jsonrpc": "2.0", "id": 1, "method": "prompts/list"},
{"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet"}},
{"jsonrpc": "2.0", "id": 2 ,"method": "prompts/get", "params": {"name": "Greet", "arguments": { "firstName": "Tobias" }}}
]

39
examples/cli/index.php Normal file
View file

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require __DIR__.'/vendor/autoload.php';
use Symfony\Component\Console as SymfonyConsole;
use Symfony\Component\Console\Output\OutputInterface;
$debug = (bool) ($_SERVER['DEBUG'] ?? false);
// Setup input, output and logger
$input = new SymfonyConsole\Input\ArgvInput($argv);
$output = new SymfonyConsole\Output\ConsoleOutput($debug ? OutputInterface::VERBOSITY_VERY_VERBOSE : OutputInterface::VERBOSITY_NORMAL);
$logger = new SymfonyConsole\Logger\ConsoleLogger($output);
// Configure the JsonRpcHandler and build the functionality
$jsonRpcHandler = new Mcp\Server\JsonRpcHandler(
new Mcp\Message\Factory(),
App\Builder::buildRequestHandlers(),
App\Builder::buildNotificationHandlers(),
$logger
);
// Set up the server
$sever = new Mcp\Server($jsonRpcHandler, $logger);
// Create the transport layer using Symfony Console
$transport = new Mcp\Server\Transport\Stdio\SymfonyConsoleTransport($input, $output);
// Start our application
$sever->connect($transport);

View file

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App;
use Mcp\Capability\PromptChain;
use Mcp\Capability\ResourceChain;
use Mcp\Capability\ToolChain;
use Mcp\Server\NotificationHandler\InitializedHandler;
use Mcp\Server\NotificationHandlerInterface;
use Mcp\Server\RequestHandler\InitializeHandler;
use Mcp\Server\RequestHandler\PingHandler;
use Mcp\Server\RequestHandler\PromptGetHandler;
use Mcp\Server\RequestHandler\PromptListHandler;
use Mcp\Server\RequestHandler\ResourceListHandler;
use Mcp\Server\RequestHandler\ResourceReadHandler;
use Mcp\Server\RequestHandler\ToolCallHandler;
use Mcp\Server\RequestHandler\ToolListHandler;
use Mcp\Server\RequestHandlerInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Builder
{
/**
* @return list<RequestHandlerInterface>
*/
public static function buildRequestHandlers(): array
{
$promptManager = new PromptChain([
new ExamplePrompt(),
]);
$resourceManager = new ResourceChain([
new ExampleResource(),
]);
$toolManager = new ToolChain([
new ExampleTool(),
]);
return [
new InitializeHandler(),
new PingHandler(),
new PromptListHandler($promptManager),
new PromptGetHandler($promptManager),
new ResourceListHandler($resourceManager),
new ResourceReadHandler($resourceManager),
new ToolCallHandler($toolManager),
new ToolListHandler($toolManager),
];
}
/**
* @return list<NotificationHandlerInterface>
*/
public static function buildNotificationHandlers(): array
{
return [
new InitializedHandler(),
];
}
}

View file

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App;
use Mcp\Capability\Prompt\MetadataInterface;
use Mcp\Capability\Prompt\PromptGet;
use Mcp\Capability\Prompt\PromptGetResult;
use Mcp\Capability\Prompt\PromptGetResultMessages;
use Mcp\Capability\Prompt\PromptGetterInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ExamplePrompt implements MetadataInterface, PromptGetterInterface
{
public function get(PromptGet $input): PromptGetResult
{
$firstName = $input->arguments['first name'] ?? null;
return new PromptGetResult(
$this->getDescription(),
[new PromptGetResultMessages(
'user',
\sprintf('Hello %s', $firstName ?? 'World')
)]
);
}
public function getName(): string
{
return 'Greet';
}
public function getDescription(): ?string
{
return 'Greet a person with a nice message';
}
public function getArguments(): array
{
return [
[
'name' => 'first name',
'description' => 'The name of the person to greet',
'required' => false,
],
];
}
}

View file

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App;
use Mcp\Capability\Resource\MetadataInterface;
use Mcp\Capability\Resource\ResourceRead;
use Mcp\Capability\Resource\ResourceReaderInterface;
use Mcp\Capability\Resource\ResourceReadResult;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ExampleResource implements MetadataInterface, ResourceReaderInterface
{
public function read(ResourceRead $input): ResourceReadResult
{
return new ResourceReadResult(
'Content of '.$this->getName(),
$this->getUri(),
);
}
public function getUri(): string
{
return 'file:///project/src/main.rs';
}
public function getName(): string
{
return 'My resource';
}
public function getDescription(): ?string
{
return 'This is just an example';
}
public function getMimeType(): ?string
{
return null;
}
public function getSize(): ?int
{
return null;
}
}

View file

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App;
use Mcp\Capability\Tool\MetadataInterface;
use Mcp\Capability\Tool\ToolCall;
use Mcp\Capability\Tool\ToolCallResult;
use Mcp\Capability\Tool\ToolExecutorInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ExampleTool implements MetadataInterface, ToolExecutorInterface
{
public function call(ToolCall $input): ToolCallResult
{
$format = $input->arguments['format'] ?? 'Y-m-d H:i:s';
return new ToolCallResult(
(new \DateTime('now', new \DateTimeZone('UTC')))->format($format)
);
}
public function getName(): string
{
return 'Current time';
}
public function getDescription(): string
{
return 'Returns the current time in UTC';
}
public function getInputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'format' => [
'type' => 'string',
'description' => 'The format of the time, e.g. "Y-m-d H:i:s"',
'default' => 'Y-m-d H:i:s',
],
],
'required' => [],
];
}
}

9
phpstan.dist.neon Normal file
View file

@ -0,0 +1,9 @@
parameters:
level: 6
paths:
- examples/
- src/
- tests/
excludePaths:
- examples/cli/vendor/* (?)
treatPhpDocTypesAsCertain: false

22
phpunit.xml.dist Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
cacheDirectory=".phpunit.cache"
colors="true"
executionOrder="depends,defects"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>

Some files were not shown because too many files have changed in this diff Show more