Skip to content

Commit 5bfd6bf

Browse files
committed
feat: MCP SDK with server implementation
0 parents  commit 5bfd6bf

34 files changed

+1250
-0
lines changed

.github/workflows/pipeline.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: pipeline
2+
on: pull_request
3+
4+
permissions:
5+
contents: read
6+
pull-requests: write
7+
8+
jobs:
9+
tests:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
php: ['8.2', '8.3', '8.4']
14+
dependencies: ['lowest', 'highest']
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup PHP
20+
uses: shivammathur/setup-php@v2
21+
with:
22+
php-version: ${{ matrix.php }}
23+
coverage: "none"
24+
25+
- name: Install Composer
26+
uses: "ramsey/composer-install@v3"
27+
with:
28+
dependency-versions: "${{ matrix.dependencies }}"
29+
30+
- name: Composer Validation
31+
run: composer validate --strict
32+
33+
- name: Install PHP Dependencies
34+
run: composer install --no-scripts
35+
36+
- name: Tests
37+
run: vendor/bin/phpunit
38+
39+
qa:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@v4
44+
45+
- name: Conventional Commit
46+
uses: ytanikin/[email protected]
47+
with:
48+
task_types: '["feat", "fix", "docs", "test", "ci", "style", "refactor", "perf", "chore", "revert"]'
49+
add_label: 'true'
50+
custom_labels: '{"feat": "feature", "fix": "bug", "docs": "documentation", "test": "test", "ci": "CI/CD", "style": "codestyle", "refactor": "refactor", "perf": "performance", "chore": "chore", "revert": "revert"}'
51+
52+
- name: Setup PHP
53+
uses: shivammathur/setup-php@v2
54+
with:
55+
php-version: '8.2'
56+
coverage: "none"
57+
58+
- name: Install Composer
59+
uses: "ramsey/composer-install@v3"
60+
61+
- name: Composer Validation
62+
run: composer validate --strict
63+
64+
- name: Install PHP Dependencies
65+
run: composer install --no-scripts
66+
67+
- name: Code Style PHP
68+
run: vendor/bin/php-cs-fixer fix --dry-run
69+
70+
- name: Rector
71+
run: vendor/bin/rector --dry-run
72+
73+
- name: PHPStan
74+
run: vendor/bin/phpstan analyse

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.phpunit.cache
2+
.php-cs-fixer.cache
3+
composer.lock
4+
vendor

.php-cs-fixer.dist.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
$finder = (new PhpCsFixer\Finder())
4+
->in(__DIR__)
5+
->exclude('var')
6+
;
7+
8+
return (new PhpCsFixer\Config())
9+
->setRules([
10+
'@Symfony' => true,
11+
])
12+
->setFinder($finder)
13+
;

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2025 Christopher Hertel
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

Makefile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.PHONY: deps-stable deps-low cs rector phpstan tests coverage run-examples ci ci-stable ci-lowest
2+
3+
deps-stable:
4+
composer update --prefer-stable
5+
6+
deps-low:
7+
composer update --prefer-lowest
8+
9+
cs:
10+
PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix --diff --verbose
11+
12+
rector:
13+
vendor/bin/rector
14+
15+
phpstan:
16+
vendor/bin/phpstan --memory-limit=-1
17+
18+
tests:
19+
vendor/bin/phpunit
20+
21+
coverage:
22+
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=coverage
23+
24+
ci: ci-stable
25+
26+
ci-stable: deps-stable rector cs phpstan tests
27+
28+
ci-lowest: deps-low rector cs phpstan tests

README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Model Context Protocol PHP SDK [WIP]
2+
3+
Model Context Protocol SDK for Client and Server applications in PHP.
4+
5+
See [Demo App](https://github.com/php-llm/mcp-demo) for a working example.
6+
7+
## Installation
8+
9+
```bash
10+
composer require php-llm/mcp-sdk
11+
```
12+
13+
## Usage with Symfony
14+
15+
Server integration points for are tailored to Symfony Console and HttpFoundation (Laravel compatible).
16+
17+
### Console Command for STDIO Server
18+
19+
```php
20+
namespace App\Command;
21+
22+
use PhpLlm\McpSdk\Server;
23+
use PhpLlm\McpSdk\Server\Transport\Stdio\SymfonyConsoleTransport;
24+
use Symfony\Component\Console\Attribute\AsCommand;
25+
use Symfony\Component\Console\Command\Command;
26+
use Symfony\Component\Console\Input\InputInterface;
27+
use Symfony\Component\Console\Output\OutputInterface;
28+
29+
#[AsCommand('mcp', 'Starts an MCP server')]
30+
final class McpCommand extends Command
31+
{
32+
public function __construct(
33+
private readonly Server $server,
34+
) {
35+
parent::__construct();
36+
}
37+
38+
protected function execute(InputInterface $input, OutputInterface $output): int
39+
{
40+
$this->server->connect(
41+
new SymfonyConsoleTransport($input, $output)
42+
);
43+
44+
return Command::SUCCESS;
45+
}
46+
}
47+
```
48+
49+
### Controller for Server-Sent Events Server
50+
51+
```php
52+
namespace App\Controller;
53+
54+
use PhpLlm\McpSdk\Server;
55+
use PhpLlm\McpSdk\Server\Transport\Sse\Store\CachePoolStore;
56+
use PhpLlm\McpSdk\Server\Transport\Sse\StreamTransport;
57+
use Symfony\Component\HttpFoundation\Request;
58+
use Symfony\Component\HttpFoundation\Response;
59+
use Symfony\Component\HttpFoundation\StreamedResponse;
60+
use Symfony\Component\HttpKernel\Attribute\AsController;
61+
use Symfony\Component\Routing\Attribute\Route;
62+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
63+
use Symfony\Component\Uid\Uuid;
64+
65+
#[AsController]
66+
#[Route('/mcp', name: 'mcp_')]
67+
final readonly class McpController
68+
{
69+
public function __construct(
70+
private Server $server,
71+
private CachePoolStore $store,
72+
private UrlGeneratorInterface $urlGenerator,
73+
) {
74+
}
75+
76+
#[Route('/sse', name: 'sse', methods: ['GET'])]
77+
public function sse(): StreamedResponse
78+
{
79+
$id = Uuid::v4();
80+
$endpoint = $this->urlGenerator->generate('mcp_messages', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL);
81+
$transport = new StreamTransport($endpoint, $this->store, $id);
82+
83+
return new StreamedResponse(fn() => $this->server->connect($transport), headers: [
84+
'Content-Type' => 'text/event-stream',
85+
'Cache-Control' => 'no-cache',
86+
'X-Accel-Buffering' => 'no',
87+
]);
88+
}
89+
90+
#[Route('/messages/{id}', name: 'messages', methods: ['POST'])]
91+
public function messages(Request $request, Uuid $id): Response
92+
{
93+
$this->store->push($id, $request->getContent());
94+
95+
return new Response();
96+
}
97+
}
98+
```

composer.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "php-llm/mcp-sdk",
3+
"type": "library",
4+
"description": "Model Context Protocol SDK for Client and Server applications in PHP",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Christopher Hertel",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"require": {
13+
"php": "^8.2",
14+
"psr/log": "^3.0",
15+
"symfony/uid": "^7.2",
16+
"php-llm/llm-chain": "^0.17.0"
17+
},
18+
"require-dev": {
19+
"php-cs-fixer/shim": "^3.70",
20+
"phpstan/phpstan": "^2.1",
21+
"phpunit/phpunit": "^11.5",
22+
"symfony/console": "^6.4 || ^7.0",
23+
"rector/rector": "^2.0"
24+
},
25+
"suggest": {
26+
"symfony/console": "To use SymfonyConsoleTransport for STDIO"
27+
},
28+
"autoload": {
29+
"psr-4": {
30+
"PhpLlm\\McpSdk\\": "src/"
31+
}
32+
},
33+
"autoload-dev": {
34+
"psr-4": {
35+
"PhpLlm\\McpSdk\\Tests\\": "tests/"
36+
}
37+
}
38+
}

phpstan.dist.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 6
3+
paths:
4+
- src/
5+
- tests/

phpunit.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
4+
cacheDirectory=".phpunit.cache"
5+
colors="true"
6+
executionOrder="depends,defects"
7+
beStrictAboutOutputDuringTests="true"
8+
failOnRisky="true"
9+
failOnWarning="true">
10+
11+
<testsuites>
12+
<testsuite name="default">
13+
<directory>tests</directory>
14+
</testsuite>
15+
</testsuites>
16+
17+
<source ignoreIndirectDeprecations="true" restrictNotices="true" restrictWarnings="true">
18+
<include>
19+
<directory>src</directory>
20+
</include>
21+
</source>
22+
</phpunit>

rector.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
7+
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitSelfCallRector;
8+
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector;
9+
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\AssertCountWithZeroToAssertEmptyRector;
10+
use Rector\PHPUnit\Set\PHPUnitSetList;
11+
12+
return RectorConfig::configure()
13+
->withPaths([
14+
__DIR__.'/src',
15+
__DIR__.'/tests',
16+
])
17+
->withPhpSets(php82: true)
18+
->withSets([
19+
PHPUnitSetList::PHPUNIT_110,
20+
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
21+
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
22+
])
23+
->withRules([
24+
PreferPHPUnitSelfCallRector::class,
25+
])
26+
->withImportNames(importShortClasses: false)
27+
->withSkip([
28+
AssertCountWithZeroToAssertEmptyRector::class,
29+
ClosureToArrowFunctionRector::class,
30+
PreferPHPUnitThisCallRector::class,
31+
])
32+
->withTypeCoverageLevel(0);

0 commit comments

Comments
 (0)