Streamline README and add conformance score badges (#312)
- Add a shields.io badge row to the README header (version, CI, PHP version, license, conformance scores, protocol/spec) to match the other official MCP SDKs. - Expand "PHP Libraries Using the MCP SDK" into a curated, alphabetically sorted list of downstream projects. - Add tests/Conformance/bin/score.php, a Symfony Console command that turns a conformance run's --output-dir into a shields.io endpoint JSON. - Extend the weekly conformance workflow to score each run and publish the client/server pass-rate to the orphan `badges` branch that the README badges read. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1300d2ee7d
commit
3e45b1f486
3 changed files with 201 additions and 5 deletions
70
.github/workflows/conformance-weekly.yaml
vendored
70
.github/workflows/conformance-weekly.yaml
vendored
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
README.md
20
README.md
|
|
@ -1,5 +1,16 @@
|
|||
# MCP PHP SDK
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://packagist.org/packages/mcp/sdk)
|
||||
[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
|
||||
[](https://packagist.org/packages/mcp/sdk)
|
||||
[](LICENSE)
|
||||
[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
|
||||
[](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.
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
116
tests/Conformance/score.php
Normal file
116
tests/Conformance/score.php
Normal file
|
|
@ -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();
|
||||
Loading…
Reference in a new issue