Skip to content

Commit b91bd85

Browse files
chr-hertelclaude
andcommitted
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) <noreply@anthropic.com>
1 parent a820497 commit b91bd85

3 files changed

Lines changed: 199 additions & 5 deletions

File tree

.github/workflows/conformance-weekly.yaml

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ name: conformance-weekly
44
# @modelcontextprotocol/conformance release. The on:pull_request pipeline
55
# pins to whatever version is available at PR time; this schedule catches
66
# upstream releases that add scenarios between PRs.
7+
#
8+
# It also scores each run and publishes the client/server pass-rate as
9+
# shields.io endpoint JSON to the orphan `badges` branch (consumed by the
10+
# README); that branch is created on the first run.
711
on:
812
schedule:
913
- cron: '0 6 * * 1' # Mondays 06:00 UTC
1014
workflow_dispatch:
1115

1216
permissions:
13-
contents: read
17+
contents: write
1418
issues: write
1519

1620
jobs:
@@ -31,7 +35,10 @@ jobs:
3135
sleep 5
3236
- name: Run conformance tests
3337
working-directory: ./tests/Conformance
34-
run: npx --yes @modelcontextprotocol/conformance@latest server --url http://localhost:8000/ --expected-failures conformance-baseline.yml
38+
run: npx --yes @modelcontextprotocol/conformance@latest server --url http://localhost:8000/ --expected-failures conformance-baseline.yml --output-dir results
39+
- name: Generate score badge
40+
if: always()
41+
run: php tests/Conformance/bin/score.php tests/Conformance/results "server conformance" server-conformance.json
3542
- name: Show docker logs on failure
3643
if: failure()
3744
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
@@ -43,6 +50,12 @@ jobs:
4350
path: |
4451
tests/Conformance/logs
4552
tests/Conformance/results
53+
- name: Upload score badge
54+
if: always()
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: server-badge
58+
path: server-conformance.json
4659

4760
client:
4861
name: conformance / client (latest)
@@ -60,7 +73,10 @@ jobs:
6073
- run: mkdir -p tests/Conformance/logs
6174
- name: Run conformance tests
6275
working-directory: ./tests/Conformance
63-
run: npx --yes @modelcontextprotocol/conformance@latest client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml
76+
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
77+
- name: Generate score badge
78+
if: always()
79+
run: php tests/Conformance/bin/score.php tests/Conformance/results "client conformance" client-conformance.json
6480
- name: Upload conformance results
6581
if: failure()
6682
uses: actions/upload-artifact@v4
@@ -69,6 +85,12 @@ jobs:
6985
path: |
7086
tests/Conformance/logs
7187
tests/Conformance/results
88+
- name: Upload score badge
89+
if: always()
90+
uses: actions/upload-artifact@v4
91+
with:
92+
name: client-badge
93+
path: client-conformance.json
7294

7395
notify:
7496
name: Open issue on failure
@@ -96,3 +118,45 @@ jobs:
96118
97119
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\`."
98120
fi
121+
122+
publish:
123+
name: Publish conformance badges
124+
runs-on: ubuntu-latest
125+
needs: [server, client]
126+
# Publish even when the suite regressed (the badge should reflect reality);
127+
# skip on forks, which cannot push the `badges` branch.
128+
if: ${{ !cancelled() && github.repository == 'modelcontextprotocol/php-sdk' }}
129+
steps:
130+
- uses: actions/checkout@v6
131+
- uses: actions/download-artifact@v4
132+
with:
133+
name: server-badge
134+
path: badges-in
135+
- uses: actions/download-artifact@v4
136+
with:
137+
name: client-badge
138+
path: badges-in
139+
- name: Publish to badges branch
140+
run: |
141+
git config user.name "github-actions[bot]"
142+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
143+
144+
if git ls-remote --exit-code --heads origin badges >/dev/null 2>&1; then
145+
git fetch origin badges
146+
git worktree add badges-wt badges
147+
else
148+
git worktree add --detach badges-wt
149+
git -C badges-wt checkout --orphan badges
150+
git -C badges-wt rm -rf --quiet . >/dev/null 2>&1 || true
151+
fi
152+
153+
cp badges-in/server-conformance.json badges-in/client-conformance.json badges-wt/
154+
155+
cd badges-wt
156+
git add -A
157+
if git diff --cached --quiet; then
158+
echo "Conformance scores unchanged."
159+
else
160+
git commit -m "Update conformance score badges"
161+
git push origin badges
162+
fi

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# MCP PHP SDK
22

3+
<div align="center">
4+
5+
[![Latest Version](https://img.shields.io/packagist/v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
6+
[![CI](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml/badge.svg)](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
7+
[![PHP Version](https://img.shields.io/packagist/php-v/mcp/sdk.svg)](https://packagist.org/packages/mcp/sdk)
8+
[![License](https://img.shields.io/packagist/l/mcp/sdk.svg)](LICENSE)
9+
[![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)
10+
[![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)
11+
12+
</div>
13+
314
The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers
415
and clients in PHP.
516

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

278289
## PHP Libraries Using the MCP SDK
279290

280-
- [pronskiy/mcp](https://github.com/pronskiy/mcp) — Additional developer experience layer
291+
- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform
292+
- [bnomei/kirby-mcp](https://github.com/bnomei/kirby-mcp) — MCP server for the Kirby CMS
293+
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP plugin exposing application functionality over MCP
294+
- [nette/mcp-inspector](https://github.com/nette/mcp-inspector) — MCP server for introspecting Nette applications
295+
- [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects
281296
- [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) — Symfony integration bundle
282-
- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP integration plugin
297+
298+
Building something on top of the SDK? Open a pull request to add it to this list.
283299

284300
## Contributing
285301

tests/Conformance/bin/score.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
/*
13+
* Turns a conformance run's results directory into a shields.io endpoint badge
14+
* JSON, so the client/server conformance score can be rendered in the README.
15+
*
16+
* php score.php <results-dir> <label> <output-file>
17+
*
18+
* The conformance CLI (run with `--output-dir`) writes one `checks.json` per
19+
* scenario. A scenario counts as passing when none of its checks has a FAILURE
20+
* status. The resulting badge message is "<passed>/<total> (<pct>%)".
21+
*/
22+
23+
use Symfony\Component\Console\Command\Command;
24+
use Symfony\Component\Console\Input\InputArgument;
25+
use Symfony\Component\Console\Input\InputInterface;
26+
use Symfony\Component\Console\Output\OutputInterface;
27+
use Symfony\Component\Console\SingleCommandApplication;
28+
use Symfony\Component\Console\Style\SymfonyStyle;
29+
30+
require_once dirname(__DIR__, 3).'/vendor/autoload.php';
31+
32+
(new SingleCommandApplication())
33+
->setName('conformance-score')
34+
->setDescription('Generates a shields.io endpoint badge from a conformance results directory')
35+
->addArgument('results-dir', InputArgument::REQUIRED, 'Directory produced by `conformance --output-dir`')
36+
->addArgument('label', InputArgument::REQUIRED, 'Badge label, e.g. "server conformance"')
37+
->addArgument('output-file', InputArgument::REQUIRED, 'Path the badge JSON is written to')
38+
->setCode(static function (InputInterface $input, OutputInterface $output): int {
39+
$io = new SymfonyStyle($input, $output);
40+
41+
$resultsDir = $input->getArgument('results-dir');
42+
$label = $input->getArgument('label');
43+
$outputFile = $input->getArgument('output-file');
44+
45+
if (!is_dir($resultsDir)) {
46+
$io->error(sprintf('Results directory "%s" does not exist.', $resultsDir));
47+
48+
return Command::FAILURE;
49+
}
50+
51+
$checkFiles = new RegexIterator(
52+
new RecursiveIteratorIterator(
53+
new RecursiveDirectoryIterator($resultsDir, FilesystemIterator::SKIP_DOTS),
54+
),
55+
'/checks\.json$/',
56+
);
57+
58+
$total = 0;
59+
$passed = 0;
60+
$failures = [];
61+
62+
foreach ($checkFiles as $file) {
63+
$checks = json_decode((string) file_get_contents($file), true);
64+
65+
if (!is_array($checks)) {
66+
$io->warning(sprintf('Skipping unreadable result file "%s".', $file));
67+
68+
continue;
69+
}
70+
71+
++$total;
72+
73+
foreach ($checks as $check) {
74+
if ('FAILURE' === ($check['status'] ?? null)) {
75+
$failures[] = basename($file->getPath());
76+
77+
continue 2;
78+
}
79+
}
80+
81+
++$passed;
82+
}
83+
84+
$pct = $total > 0 ? (int) round($passed / $total * 100) : 0;
85+
86+
$badge = [
87+
'schemaVersion' => 1,
88+
'label' => $label,
89+
'message' => $total > 0 ? sprintf('%d/%d (%d%%)', $passed, $total, $pct) : 'no data',
90+
'color' => match (true) {
91+
0 === $total => 'lightgrey',
92+
$pct >= 95 => 'brightgreen',
93+
$pct >= 80 => 'green',
94+
$pct >= 60 => 'yellow',
95+
default => 'orange',
96+
},
97+
];
98+
99+
if (false === file_put_contents($outputFile, json_encode($badge, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n")) {
100+
$io->error(sprintf('Could not write badge file "%s".', $outputFile));
101+
102+
return Command::FAILURE;
103+
}
104+
105+
if ($failures && $io->isVerbose()) {
106+
$io->section('Failing scenarios');
107+
$io->listing($failures);
108+
}
109+
110+
$io->success(sprintf('%s: %s', $label, $badge['message']));
111+
112+
return Command::SUCCESS;
113+
})
114+
->run();

0 commit comments

Comments
 (0)