Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions .github/workflows/conformance-weekly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ name: conformance-weekly
# @modelcontextprotocol/conformance release. The on:pull_request pipeline
# pins to whatever version is available at PR time; this schedule catches
# upstream releases that add scenarios between PRs.
#
# It also scores each run and publishes the client/server pass-rate as
# shields.io endpoint JSON to the orphan `badges` branch (consumed by the
# README); that branch is created on the first run.
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:

permissions:
contents: read
contents: write
issues: write

jobs:
Expand All @@ -31,7 +35,10 @@ jobs:
sleep 5
- name: Run conformance tests
working-directory: ./tests/Conformance
run: npx --yes @modelcontextprotocol/conformance@latest server --url http://localhost:8000/ --expected-failures conformance-baseline.yml
run: npx --yes @modelcontextprotocol/conformance@latest server --url http://localhost:8000/ --expected-failures conformance-baseline.yml --output-dir results
- name: Generate score badge
if: always()
run: php tests/Conformance/score.php server
- name: Show docker logs on failure
if: failure()
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
Expand All @@ -43,6 +50,12 @@ jobs:
path: |
tests/Conformance/logs
tests/Conformance/results
- name: Upload score badge
if: always()
uses: actions/upload-artifact@v4
with:
name: server-badge
path: tests/Conformance/server-conformance.json

client:
name: conformance / client (latest)
Expand All @@ -60,7 +73,10 @@ jobs:
- run: mkdir -p tests/Conformance/logs
- name: Run conformance tests
working-directory: ./tests/Conformance
run: npx --yes @modelcontextprotocol/conformance@latest client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml
run: npx --yes @modelcontextprotocol/conformance@latest client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml --output-dir results
- name: Generate score badge
if: always()
run: php tests/Conformance/score.php client
- name: Upload conformance results
if: failure()
uses: actions/upload-artifact@v4
Expand All @@ -69,6 +85,12 @@ jobs:
path: |
tests/Conformance/logs
tests/Conformance/results
- name: Upload score badge
if: always()
uses: actions/upload-artifact@v4
with:
name: client-badge
path: tests/Conformance/client-conformance.json

notify:
name: Open issue on failure
Expand Down Expand Up @@ -96,3 +118,45 @@ jobs:

Upstream likely published a release whose scenarios the SDK does not satisfy. Either fix the SDK, update the conformance fixtures, or add the new failure to \`tests/Conformance/conformance-baseline.yml\`."
fi

publish:
name: Publish conformance badges
runs-on: ubuntu-latest
needs: [server, client]
# Publish even when the suite regressed (the badge should reflect reality);
# skip on forks, which cannot push the `badges` branch.
if: ${{ !cancelled() && github.repository == 'modelcontextprotocol/php-sdk' }}
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
name: server-badge
path: badges-in
- uses: actions/download-artifact@v4
with:
name: client-badge
path: badges-in
- name: Publish to badges branch
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if git ls-remote --exit-code --heads origin badges >/dev/null 2>&1; then
git fetch origin badges
git worktree add badges-wt badges
else
git worktree add --detach badges-wt
git -C badges-wt checkout --orphan badges
git -C badges-wt rm -rf --quiet . >/dev/null 2>&1 || true
fi

cp badges-in/server-conformance.json badges-in/client-conformance.json badges-wt/

cd badges-wt
git add -A
if git diff --cached --quiet; then
echo "Conformance scores unchanged."
else
git commit -m "Update conformance score badges"
git push origin badges
fi
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# MCP PHP SDK

<div align="center">

[![Latest Version](https://img.shields.io/packagist/v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
[![CI](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml/badge.svg)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
[![PHP Version](https://img.shields.io/packagist/php-v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
[![License](https://img.shields.io/packagist/l/mcp/sdk.svg)](LICENSE)
[![Server Conformance](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/server-conformance.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
[![Client Conformance](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/modelcontextprotocol/php-sdk/badges/client-conformance.json)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)

</div>

The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers
and clients in PHP.

Expand Down Expand Up @@ -277,9 +288,14 @@ $client->connect($transport);

## PHP Libraries Using the MCP SDK

- [pronskiy/mcp](https://github.com/pronskiy/mcp) — Additional developer experience layer
- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform
- [bnomei/kirby-mcp](https://github.com/bnomei/kirby-mcp) — MCP server for the Kirby CMS
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP plugin exposing application functionality over MCP
- [nette/mcp-inspector](https://github.com/nette/mcp-inspector) — MCP server for introspecting Nette applications
- [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects
- [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) — Symfony integration bundle
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP integration plugin

Building something on top of the SDK? Open a pull request to add it to this list.

## Contributing

Expand Down
116 changes: 116 additions & 0 deletions tests/Conformance/score.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?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.
*/

/*
* Turns a conformance run's results into a shields.io endpoint badge JSON, so
* the client/server conformance score can be rendered in the README.
*
* php score.php <server|client>
*
* The conformance CLI (run with `--output-dir results`) writes one
* `checks.json` per scenario into the `results/` directory next to this file.
* A scenario counts as passing when none of its checks has a FAILURE status;
* the badge message is "<passed>/<total> (<pct>%)" and is written to
* `<suite>-conformance.json`.
*/

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SingleCommandApplication;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;

require_once dirname(__DIR__, 2).'/vendor/autoload.php';

(new SingleCommandApplication())
->setName('conformance-score')
->setDescription('Generates a shields.io endpoint badge from the conformance results')
->addArgument('suite', InputArgument::REQUIRED, 'Which conformance suite was run: "server" or "client"')
->setCode(static function (InputInterface $input, OutputInterface $output): int {
$io = new SymfonyStyle($input, $output);

$suite = $input->getArgument('suite');

if (!in_array($suite, ['server', 'client'], true)) {
$io->error(sprintf('Suite must be "server" or "client", got "%s".', $suite));

return Command::INVALID;
}

$resultsDir = __DIR__.'/results';

if (!is_dir($resultsDir)) {
$io->error(sprintf('Results directory "%s" does not exist; run the conformance suite with `--output-dir results` first.', $resultsDir));

return Command::FAILURE;
}

$total = 0;
$passed = 0;
$failures = [];

foreach (Finder::create()->files()->name('checks.json')->in($resultsDir) as $file) {
$checks = json_decode($file->getContents(), true);

if (!is_array($checks)) {
$io->warning(sprintf('Skipping unreadable result file "%s".', $file->getRelativePathname()));

continue;
}

++$total;

foreach ($checks as $check) {
if ('FAILURE' === ($check['status'] ?? null)) {
$failures[] = $file->getRelativePath();

continue 2;
}
}

++$passed;
}

$pct = $total > 0 ? (int) round($passed / $total * 100) : 0;

$badge = [
'schemaVersion' => 1,
'label' => $suite.' conformance',
'message' => $total > 0 ? sprintf('%d/%d (%d%%)', $passed, $total, $pct) : 'no data',
'color' => match (true) {
0 === $total => 'lightgrey',
$pct >= 95 => 'brightgreen',
$pct >= 80 => 'green',
$pct >= 60 => 'yellow',
default => 'orange',
},
];

$outputFile = __DIR__.'/'.$suite.'-conformance.json';

if (false === file_put_contents($outputFile, json_encode($badge, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n")) {
$io->error(sprintf('Could not write badge file "%s".', $outputFile));

return Command::FAILURE;
}

if ($failures && $io->isVerbose()) {
$io->section('Failing scenarios');
$io->listing($failures);
}

$io->success(sprintf('%s: %s', $badge['label'], $badge['message']));

return Command::SUCCESS;
})
->run();