From 4305dd572d77129a1c647846948b4966eb3a3f72 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Sun, 17 May 2026 18:12:05 +0200 Subject: [PATCH] Streamline README and add conformance score badges - 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) --- .github/workflows/conformance-weekly.yaml | 70 ++++++++++++- README.md | 20 +++- tests/Conformance/score.php | 116 ++++++++++++++++++++++ 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 tests/Conformance/score.php diff --git a/.github/workflows/conformance-weekly.yaml b/.github/workflows/conformance-weekly.yaml index 685b9396..d55ac819 100644 --- a/.github/workflows/conformance-weekly.yaml +++ b/.github/workflows/conformance-weekly.yaml @@ -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 diff --git a/README.md b/README.md index 37335b1e..bc97892b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # MCP PHP SDK +
+ +[![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) + +
+ 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 diff --git a/tests/Conformance/score.php b/tests/Conformance/score.php new file mode 100644 index 00000000..b33df91e --- /dev/null +++ b/tests/Conformance/score.php @@ -0,0 +1,116 @@ + + * + * 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 "/ (%)" and is written to + * `-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();