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
+
+
+[](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)
+
+
+
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();