Compare commits
392 changed files with 4568 additions and 29908 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -5,8 +5,6 @@
|
||||||
/tests export-ignore
|
/tests export-ignore
|
||||||
/.php-cs-fixer.dist.php export-ignore
|
/.php-cs-fixer.dist.php export-ignore
|
||||||
/Makefile export-ignore
|
/Makefile export-ignore
|
||||||
/mkdocs.yml export-ignore
|
|
||||||
/requirements-docs.txt export-ignore
|
|
||||||
/phpdoc.dist.xml
|
/phpdoc.dist.xml
|
||||||
/phpstan* export-ignore
|
/phpstan* export-ignore
|
||||||
/phpunit.xml.dist export-ignore
|
/phpunit.xml.dist export-ignore
|
||||||
|
|
|
||||||
91
.github/workflows/conformance-weekly.yaml
vendored
91
.github/workflows/conformance-weekly.yaml
vendored
|
|
@ -1,8 +1,13 @@
|
||||||
name: conformance-weekly
|
name: conformance-weekly
|
||||||
|
|
||||||
# Tracks the newest published @modelcontextprotocol/conformance; the
|
# Runs the MCP conformance suite weekly against the latest
|
||||||
# on:pull_request pipeline pins an exact version instead. Also publishes
|
# @modelcontextprotocol/conformance release. The on:pull_request pipeline
|
||||||
# score badges to the orphan `badges` branch, consumed by the README.
|
# 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:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 6 * * 1' # Mondays 06:00 UTC
|
- cron: '0 6 * * 1' # Mondays 06:00 UTC
|
||||||
|
|
@ -14,20 +19,8 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server:
|
server:
|
||||||
name: conformance / server (${{ matrix.spec-version }}, ${{ matrix.dist-tag }})
|
name: conformance / server (latest)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- spec-version: '2025-11-25'
|
|
||||||
dist-tag: latest
|
|
||||||
path: '/'
|
|
||||||
baseline: conformance-baseline-2025-11-25.yml
|
|
||||||
- spec-version: '2026-07-28'
|
|
||||||
dist-tag: alpha
|
|
||||||
path: '/'
|
|
||||||
baseline: conformance-baseline-2026-07-28.yml
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
|
|
@ -42,16 +35,10 @@ jobs:
|
||||||
sleep 5
|
sleep 5
|
||||||
- name: Run conformance tests
|
- name: Run conformance tests
|
||||||
working-directory: ./tests/Conformance
|
working-directory: ./tests/Conformance
|
||||||
run: >-
|
run: npx --yes @modelcontextprotocol/conformance@latest server --url http://localhost:8000/ --expected-failures conformance-baseline.yml --output-dir results
|
||||||
npx --yes @modelcontextprotocol/conformance@${{ matrix.dist-tag }} server
|
|
||||||
--url http://localhost:8000${{ matrix.path }}
|
|
||||||
--suite all
|
|
||||||
--spec-version ${{ matrix.spec-version }}
|
|
||||||
--expected-failures ${{ matrix.baseline }}
|
|
||||||
--output-dir results
|
|
||||||
- name: Generate score badge
|
- name: Generate score badge
|
||||||
if: always()
|
if: always()
|
||||||
run: php tests/Conformance/score.php server ${{ matrix.spec-version }}
|
run: php tests/Conformance/score.php server
|
||||||
- name: Show docker logs on failure
|
- name: Show docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
|
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml logs
|
||||||
|
|
@ -59,7 +46,7 @@ jobs:
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: conformance-server-results-${{ matrix.spec-version }}
|
name: conformance-server-results
|
||||||
path: |
|
path: |
|
||||||
tests/Conformance/logs
|
tests/Conformance/logs
|
||||||
tests/Conformance/results
|
tests/Conformance/results
|
||||||
|
|
@ -67,22 +54,12 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: badge-server-${{ matrix.spec-version }}
|
name: server-badge
|
||||||
path: tests/Conformance/server-conformance-${{ matrix.spec-version }}.json
|
path: tests/Conformance/server-conformance.json
|
||||||
|
|
||||||
client:
|
client:
|
||||||
name: conformance / client (${{ matrix.spec-version }}, ${{ matrix.dist-tag }})
|
name: conformance / client (latest)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- spec-version: '2025-11-25'
|
|
||||||
dist-tag: latest
|
|
||||||
baseline: conformance-baseline-2025-11-25.yml
|
|
||||||
- spec-version: '2026-07-28'
|
|
||||||
dist-tag: alpha
|
|
||||||
baseline: conformance-baseline-2026-07-28.yml
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
|
|
@ -96,21 +73,15 @@ jobs:
|
||||||
- run: mkdir -p tests/Conformance/logs
|
- run: mkdir -p tests/Conformance/logs
|
||||||
- name: Run conformance tests
|
- name: Run conformance tests
|
||||||
working-directory: ./tests/Conformance
|
working-directory: ./tests/Conformance
|
||||||
run: >-
|
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
|
||||||
npx --yes @modelcontextprotocol/conformance@${{ matrix.dist-tag }} client
|
|
||||||
--command "php ${{ github.workspace }}/tests/Conformance/client.php"
|
|
||||||
--suite all
|
|
||||||
--spec-version ${{ matrix.spec-version }}
|
|
||||||
--expected-failures ${{ matrix.baseline }}
|
|
||||||
--output-dir results
|
|
||||||
- name: Generate score badge
|
- name: Generate score badge
|
||||||
if: always()
|
if: always()
|
||||||
run: php tests/Conformance/score.php client ${{ matrix.spec-version }}
|
run: php tests/Conformance/score.php client
|
||||||
- name: Upload conformance results
|
- name: Upload conformance results
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: conformance-client-results-${{ matrix.spec-version }}
|
name: conformance-client-results
|
||||||
path: |
|
path: |
|
||||||
tests/Conformance/logs
|
tests/Conformance/logs
|
||||||
tests/Conformance/results
|
tests/Conformance/results
|
||||||
|
|
@ -118,18 +89,17 @@ jobs:
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: badge-client-${{ matrix.spec-version }}
|
name: client-badge
|
||||||
path: tests/Conformance/client-conformance-${{ matrix.spec-version }}.json
|
path: tests/Conformance/client-conformance.json
|
||||||
|
|
||||||
notify:
|
notify:
|
||||||
name: Open issue on failure
|
name: Open issue on failure
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [server, client]
|
needs: [server, client]
|
||||||
if: failure()
|
if: failure() && github.event_name == 'schedule'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GH_REPO: ${{ github.repository }}
|
GH_REPO: ${{ github.repository }}
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
|
||||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
steps:
|
steps:
|
||||||
- name: File or comment tracking issue
|
- name: File or comment tracking issue
|
||||||
|
|
@ -141,27 +111,31 @@ jobs:
|
||||||
gh issue create \
|
gh issue create \
|
||||||
--title '[conformance] Weekly conformance run failed' \
|
--title '[conformance] Weekly conformance run failed' \
|
||||||
--label conformance-weekly \
|
--label conformance-weekly \
|
||||||
--body "Conformance against the newest published \`@modelcontextprotocol/conformance\` failed.
|
--body "Weekly conformance against \`@modelcontextprotocol/conformance@latest\` failed.
|
||||||
|
|
||||||
- Run: $RUN_URL
|
- Run: $RUN_URL
|
||||||
- Trigger: $EVENT_NAME
|
- Triggered: $(date -u +%FT%TZ)
|
||||||
- Date: $(date -u +%FT%TZ)
|
|
||||||
|
|
||||||
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 the matching \`tests/Conformance/conformance-baseline-<spec-version>.yml\`."
|
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
|
fi
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish conformance badges
|
name: Publish conformance badges
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [server, client]
|
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' }}
|
if: ${{ !cancelled() && github.repository == 'modelcontextprotocol/php-sdk' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/download-artifact@v8
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
pattern: badge-*
|
name: server-badge
|
||||||
|
path: badges-in
|
||||||
|
- uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: client-badge
|
||||||
path: badges-in
|
path: badges-in
|
||||||
merge-multiple: true
|
|
||||||
- name: Publish to badges branch
|
- name: Publish to badges branch
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
|
|
@ -176,8 +150,7 @@ jobs:
|
||||||
git -C badges-wt rm -rf --quiet . >/dev/null 2>&1 || true
|
git -C badges-wt rm -rf --quiet . >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f badges-wt/server-conformance.json badges-wt/client-conformance.json
|
cp badges-in/server-conformance.json badges-in/client-conformance.json badges-wt/
|
||||||
cp badges-in/*.json badges-wt/
|
|
||||||
|
|
||||||
cd badges-wt
|
cd badges-wt
|
||||||
git add -A
|
git add -A
|
||||||
|
|
|
||||||
53
.github/workflows/docs.yml
vendored
53
.github/workflows/docs.yml
vendored
|
|
@ -1,25 +1,15 @@
|
||||||
name: Documentation
|
name: Deploy Documentation
|
||||||
|
|
||||||
# The site is built by `make docs`: Zensical renders the guides under docs/
|
|
||||||
# (see mkdocs.yml) and phpDocumentor renders the API reference into /api/.
|
|
||||||
# Pull requests build only — the build is strict, so a broken internal link
|
|
||||||
# fails CI instead of shipping a dead link to the site.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
release:
|
||||||
branches: [main]
|
types: [published]
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: docs-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -31,33 +21,16 @@ jobs:
|
||||||
php-version: '8.4'
|
php-version: '8.4'
|
||||||
coverage: "none"
|
coverage: "none"
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
- name: Install Composer
|
||||||
uses: "ramsey/composer-install@v4"
|
uses: "ramsey/composer-install@v4"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Generate Documentation
|
||||||
uses: astral-sh/setup-uv@v10.1.0
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
|
|
||||||
- name: Build documentation
|
|
||||||
run: make docs
|
run: make docs
|
||||||
|
|
||||||
- name: Upload Pages artifact
|
- name: Deploy to gh-pages branch
|
||||||
uses: actions/upload-pages-artifact@v5
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
with:
|
with:
|
||||||
path: ./site
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./.phpdoc/build
|
||||||
deploy:
|
enable_jekyll: false
|
||||||
needs: build
|
cname: php.sdk.modelcontextprotocol.io
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v5
|
|
||||||
|
|
|
||||||
68
.github/workflows/pipeline.yaml
vendored
68
.github/workflows/pipeline.yaml
vendored
|
|
@ -5,10 +5,6 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
|
||||||
# Pinned exact version; conformance-weekly.yaml tracks upstream releases.
|
|
||||||
CONFORMANCE_PKG: "@modelcontextprotocol/conformance@0.2.0-alpha.11"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -53,28 +49,6 @@ jobs:
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: vendor/bin/phpunit --testsuite=unit
|
run: vendor/bin/phpunit --testsuite=unit
|
||||||
|
|
||||||
integration:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
php: ['8.1', '8.2', '8.3', '8.4', '8.5']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php }}
|
|
||||||
coverage: "none"
|
|
||||||
|
|
||||||
- name: Install Composer
|
|
||||||
uses: "ramsey/composer-install@v4"
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
run: vendor/bin/phpunit --testsuite=integration
|
|
||||||
|
|
||||||
inspector:
|
inspector:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -99,19 +73,8 @@ jobs:
|
||||||
run: vendor/bin/phpunit --testsuite=inspector
|
run: vendor/bin/phpunit --testsuite=inspector
|
||||||
|
|
||||||
conformance-server:
|
conformance-server:
|
||||||
name: conformance / server (${{ matrix.spec-version }})
|
name: conformance / server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- spec-version: '2025-11-25'
|
|
||||||
path: '/'
|
|
||||||
baseline: conformance-baseline-2025-11-25.yml
|
|
||||||
- spec-version: '2026-07-28'
|
|
||||||
path: '/'
|
|
||||||
baseline: conformance-baseline-2026-07-28.yml
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
@ -133,12 +96,7 @@ jobs:
|
||||||
|
|
||||||
- name: Run conformance tests
|
- name: Run conformance tests
|
||||||
working-directory: ./tests/Conformance
|
working-directory: ./tests/Conformance
|
||||||
run: >-
|
run: npx @modelcontextprotocol/conformance server --url http://localhost:8000/ --expected-failures conformance-baseline.yml
|
||||||
npx --yes ${{ env.CONFORMANCE_PKG }} server
|
|
||||||
--url http://localhost:8000${{ matrix.path }}
|
|
||||||
--suite all
|
|
||||||
--spec-version ${{ matrix.spec-version }}
|
|
||||||
--expected-failures ${{ matrix.baseline }}
|
|
||||||
|
|
||||||
- name: Show logs on failure
|
- name: Show logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
@ -166,17 +124,8 @@ jobs:
|
||||||
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
|
run: docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
|
||||||
|
|
||||||
conformance-client:
|
conformance-client:
|
||||||
name: conformance / client (${{ matrix.spec-version }})
|
name: conformance / client
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- spec-version: '2025-11-25'
|
|
||||||
baseline: conformance-baseline-2025-11-25.yml
|
|
||||||
- spec-version: '2026-07-28'
|
|
||||||
baseline: conformance-baseline-2026-07-28.yml
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
@ -200,12 +149,7 @@ jobs:
|
||||||
|
|
||||||
- name: Run client conformance tests
|
- name: Run client conformance tests
|
||||||
working-directory: ./tests/Conformance
|
working-directory: ./tests/Conformance
|
||||||
run: >-
|
run: npx @modelcontextprotocol/conformance client --command "php ${{ github.workspace }}/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml
|
||||||
npx --yes ${{ env.CONFORMANCE_PKG }} client
|
|
||||||
--command "php ${{ github.workspace }}/tests/Conformance/client.php"
|
|
||||||
--suite all
|
|
||||||
--spec-version ${{ matrix.spec-version }}
|
|
||||||
--expected-failures ${{ matrix.baseline }}
|
|
||||||
|
|
||||||
- name: Show logs on failure
|
- name: Show logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
@ -244,7 +188,5 @@ jobs:
|
||||||
- name: PHPStan
|
- name: PHPStan
|
||||||
run: vendor/bin/phpstan analyse
|
run: vendor/bin/phpstan analyse
|
||||||
|
|
||||||
# Only the phpDocumentor half: this job is PHP-only, and the Zensical
|
|
||||||
# guides are built (strictly) by the Documentation workflow.
|
|
||||||
- name: Documentation
|
- name: Documentation
|
||||||
run: make docs-api
|
run: make docs
|
||||||
|
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -8,14 +8,10 @@ examples/**/cache
|
||||||
examples/**/sessions
|
examples/**/sessions
|
||||||
tests/Conformance/client-conformance.json
|
tests/Conformance/client-conformance.json
|
||||||
tests/Conformance/server-conformance.json
|
tests/Conformance/server-conformance.json
|
||||||
tests/Conformance/results*
|
tests/Conformance/results
|
||||||
tests/Conformance/sessions
|
tests/Conformance/sessions
|
||||||
tests/Conformance/logs/*.log
|
tests/Conformance/logs/*.log
|
||||||
|
|
||||||
# phpDocumentor
|
# phpDocumentor
|
||||||
.phpdoc/build/
|
.phpdoc/build/
|
||||||
.phpdoc/cache/
|
.phpdoc/cache/
|
||||||
|
|
||||||
# Documentation site (make docs)
|
|
||||||
/site/
|
|
||||||
/.cache/
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% set topMenu = {
|
{% set topMenu = {
|
||||||
"menu": [
|
"menu": [
|
||||||
{ "name": "Guides", "url": "/"},
|
{ "name": "Guides", "url": "docs/index.html"},
|
||||||
{ "name": "Specification", "url": "https://modelcontextprotocol.io/" }
|
{ "name": "Specification", "url": "https://modelcontextprotocol.io/" }
|
||||||
],
|
],
|
||||||
"social": [
|
"social": [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<h1 class="phpdocumentor-title">
|
<h1 class="phpdocumentor-title">
|
||||||
<a href="/" class="phpdocumentor-title__link">
|
<a href="{{ path('/docs/index.html') }}" class="phpdocumentor-title__link">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 195 195" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 195 195" fill="none">
|
||||||
<path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
<path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
<path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
<path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
|
|
||||||
39
CHANGELOG.md
39
CHANGELOG.md
|
|
@ -2,45 +2,6 @@
|
||||||
|
|
||||||
All notable changes to `mcp/sdk` will be documented in this file.
|
All notable changes to `mcp/sdk` will be documented in this file.
|
||||||
|
|
||||||
0.9.0
|
|
||||||
-----
|
|
||||||
|
|
||||||
* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument.
|
|
||||||
* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions.
|
|
||||||
* Fix OIDC discovery rejecting issuers with a trailing slash (e.g. Authentik, Auth0).
|
|
||||||
|
|
||||||
0.8.0
|
|
||||||
-----
|
|
||||||
|
|
||||||
* Support for MCP spec version 2026-07-28 incl. stateless HTTP transport
|
|
||||||
* Negotiate the protocol revision during `initialize`: the server counter-offers a revision it supports and the client fails the handshake instead of continuing on an unagreed one. Adds `Client::getProtocolVersion()`.
|
|
||||||
* Serve both protocol eras from one endpoint: `StreamableHttpTransport` classifies each request (`InboundClassifier`) and routes it to the matching dispatcher, so one URL answers modern and handshake-era clients alike. `Builder::withoutModernEra()` opts out, `Builder::setModernVersions()` narrows the modern leg.
|
|
||||||
* Speak the lifecycle from the client: `Client` opens with `server/discover`, stamps `_meta` with version, capabilities and client info, and sends the `Mcp-Method`/`Mcp-Name`/`Mcp-Param-*` headers (via `Client\Stateless\ToolCatalog`). `Schema\Wire\McpHeader` holds the shared header names.
|
|
||||||
* Add multi round-trip requests (SEP-2322): a handler returning `InputRequiredResult` yields `resultType: "input_required"` with an opaque, signed `requestState` (`RequestStateCodec`, key via `Builder::setRequestState()`); the client retries with `inputResponses`, read through `RequestContext::getInputContext()` and its typed `elicitResult()`/`samplingResult()`/`rootsResult()`. On the client, `InputRequestResolver` answers such results automatically from the host's elicitation, sampling and roots handlers.
|
|
||||||
* Serve `ClientGateway::elicit()`/`elicitUrl()` on every revision: where the client cannot be asked mid-request, `Server\Stateless\ElicitationReplay` turns the ask into `input_required` and resumes once re-sent — the handler is entered once per ask. `InputRequiredShim` does the reverse for handshake-era clients. `sample()`/`listRoots()` raise a `LogicException` on `2026-07-28`, which removed them.
|
|
||||||
* Run stateless handlers in a fiber so `$gateway->progress()`/`log()` stream over SSE when the handler emits something and the client accepts `text/event-stream`; honour `io.modelcontextprotocol/logLevel` (SEP-2575). Adds `LoggingLevel::severity()`/`isAtLeast()`.
|
|
||||||
* Deliver notifications on `subscriptions/listen` (SEP-2575) via `NotificationBusInterface` — `InMemoryNotificationBus` for persistent runtimes, `Psr16NotificationBus` for PHP-FPM — set with `Builder::setNotificationBus()`; `Builder::setSubscriptionLifetime()` replaces the hard-coded 30s ceiling.
|
|
||||||
* Validate the standard request headers (SEP-2243) with `StandardHeaderValidator` (`Builder::setHeaderValidator()`), answering `-32020` when they contradict the body.
|
|
||||||
* Add `Wire\CachePolicy` (`Builder::setCachePolicy()`) for SEP-2549 caching hints; defaults to `ttlMs: 0, cacheScope: private`. A `ReadResourceResult` may override with its own values.
|
|
||||||
* Carry W3C trace context (SEP-414): `traceparent`/`tracestate`/`baggage` from `_meta` are exposed via `RequestContext::getTraceContext()` and echoed onto the request's notifications.
|
|
||||||
* Close the schema gaps for `2025-06-18`/`2025-11-25` and add the `2026-07-28` surface (SEP-2106): url-mode elicitation (`ClientGateway::elicitUrl()`/`supportsElicitationUrl()`), `Implementation::title`, and `outputSchema`/`structuredContent` accepting any JSON value.
|
|
||||||
* Deprecate Roots, Sampling and Logging (SEP-2577, earliest removal `2027-07-28`); they keep working but trigger a deprecation notice.
|
|
||||||
* [BC Break] Answer a not-found subject with `-32602` instead of `-32002` (SEP-2164): `resources/read` picks the code by revision (`-32602` from `2026-07-28` on), `prompts/get`, `completion/complete` and `tools/call` switch on every revision. Adds `ProtocolVersion::usesInvalidParamsForResourceNotFound()`.
|
|
||||||
* [BC Break] A list-shaped tool result is only sent as `structuredContent` on `2026-07-28`+; older revisions keep the JSON-encoded value in `content`.
|
|
||||||
* [BC Break] Add the extensions framework (SEP-2133) MCP Apps sits on: `ExtensionInterface::getId()` returns an `ExtensionIdentifier` value object, and the interface gains `getMessages()`/`getRequestHandlers()` (extend `AbstractExtension` to skip both). `MessageFactory::make()` takes an `$additional` message list; `RequestHandlerInterface`'s result template is covariant. `ServerExtensionInterface` is replaced by the side-agnostic `Schema\Extension\ExtensionInterface`.
|
|
||||||
* Add client-side extension negotiation: `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, `ClientCapabilities::withExtensions()`.
|
|
||||||
* Add sampling-with-tools: sampling requests can carry tools and tool-choice preferences, messages support tool-use/tool-result blocks, and clients advertise `sampling.context`/`sampling.tools` (`ClientGateway::supportsSamplingTools()`/`supportsSamplingContext()`). Requests violating the tool-flow rules are rejected with a JSON-RPC error.
|
|
||||||
* [BC Break] `SamplingMessage::$content` and `CreateSamplingMessageResult::$content` may be a list of content blocks — use `getContentBlocks()`. `CreateSamplingMessageResult` rejects roles other than `assistant` and empty content.
|
|
||||||
* Add client-side Roots support (`RootsCallbackInterface`, `Client::sendRootsListChanged()`) and server-side `ClientGateway::listRoots()`/`supportsRoots()`/`supportsSampling()`.
|
|
||||||
* Add `Schema\Content\ResourceLink` to reference a resource by URI in tool results and prompt messages.
|
|
||||||
* [BC Break] `Schema\JsonRpc\Error` accepts `null` as `$id`; an unreadable id now omits the member instead of sending `"id": ""`. `MessageFactory` decodes a missing or null id as an id-less error.
|
|
||||||
* Preserve the request `id` on an invalid-but-parseable message (`-32600`) via `InvalidInputMessageException::getRequestId()`.
|
|
||||||
* [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name`; the spec allows any string.
|
|
||||||
* Log expected tool failures (`ToolCallException`) at debug level instead of error.
|
|
||||||
* Add `annotations` to `ImageContent`.
|
|
||||||
* Fix empty tool/resource schemas serializing as `[]` instead of `{}`.
|
|
||||||
* Fix `PromptResultFormatter` dropping `annotations`, `_meta` and `mimeType` for plain-array content.
|
|
||||||
|
|
||||||
0.7.0
|
0.7.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
# Contributing
|
|
||||||
|
|
||||||
Thanks for considering a contribution to the PHP SDK for the Model Context Protocol. This is a
|
|
||||||
collaboration between [the PHP Foundation](https://thephp.foundation/) and the
|
|
||||||
[Symfony project](https://symfony.com/), and it follows Symfony's conventions throughout.
|
|
||||||
|
|
||||||
## Ways to contribute
|
|
||||||
|
|
||||||
- **Report a bug** or **propose a feature** by [opening an issue](https://github.com/modelcontextprotocol/php-sdk/issues).
|
|
||||||
Check existing issues first — many spec-driven changes are already tracked under the relevant
|
|
||||||
`2026-07-28`-style release label.
|
|
||||||
- **Send a pull request** for a fix, a new capability, or a docs improvement.
|
|
||||||
- **Improve the guides** under `docs/` — see [Documentation](#documentation) below for how they're built.
|
|
||||||
- **Help close a conformance gap** — `make conformance-tests` runs the official MCP conformance
|
|
||||||
suite against this SDK; a failing scenario there is a concrete, well-scoped contribution.
|
|
||||||
|
|
||||||
## Development setup
|
|
||||||
|
|
||||||
Requires PHP 8.1+.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Before opening a pull request
|
|
||||||
|
|
||||||
Run the full CI suite locally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make ci
|
|
||||||
```
|
|
||||||
|
|
||||||
This runs, in order: `make cs` (PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis),
|
|
||||||
and `make tests` (unit + inspector tests). All three must pass. If your change touches
|
|
||||||
protocol-observable behavior, also run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make conformance-tests # requires Docker
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coding standards
|
|
||||||
|
|
||||||
This project follows [Symfony's coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
|
|
||||||
and [backward compatibility promise](https://symfony.com/doc/current/contributing/code/bc.html). In short:
|
|
||||||
|
|
||||||
See [CLAUDE.md](CLAUDE.md) for a fuller tour of the codebase's architecture and layout.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
New capabilities need unit tests (`tests/Unit/`) covering the core logic, and — for anything
|
|
||||||
reachable over the wire — inspector tests (`tests/Inspector/`) for end-to-end coverage. If you're
|
|
||||||
adding a documented pattern, consider adding or updating an example under `examples/`.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The guides under `docs/` are built with [Zensical](https://zensical.org/) in `--strict` mode,
|
|
||||||
which fails the build on a broken internal link:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make docs-guides
|
|
||||||
```
|
|
||||||
|
|
||||||
Links between guide pages must be relative paths that resolve within `docs/` (e.g.
|
|
||||||
`protocol-versions.md`, `../CLAUDE.md` will *not* resolve — Zensical only follows the `docs/` tree).
|
|
||||||
For anything at the repo root (`CLAUDE.md`, `ROADMAP.md`, `CHANGELOG.md`), link to it by its GitHub
|
|
||||||
URL instead, matching the pattern already used across `docs/`. The class-level API reference is
|
|
||||||
generated separately by phpDocumentor (`make docs-api`) and isn't hand-written.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
The SDK follows [Semantic Versioning](https://semver.org/) — see
|
|
||||||
[SDK tier target](docs/sdk-tier.md#versioning) for what that means pre- and post-1.0, and how it
|
|
||||||
lines up with Symfony's backward compatibility promise.
|
|
||||||
|
|
||||||
## Licensing
|
|
||||||
|
|
||||||
New contributions are licensed under Apache License, Version 2.0. Existing code predating this
|
|
||||||
policy remains under the MIT License — see [LICENSE](LICENSE) for the details. By opening a pull
|
|
||||||
request, you agree your contribution is provided under those terms.
|
|
||||||
|
|
||||||
## Getting help
|
|
||||||
|
|
||||||
If something is unclear or you want early feedback on an approach before writing code, open an
|
|
||||||
issue or a draft pull request — that's the right place to ask, rather than guessing at scope.
|
|
||||||
62
Makefile
62
Makefile
|
|
@ -1,15 +1,4 @@
|
||||||
.PHONY: deps-stable deps-low cs phpstan tests unit-tests integration-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client conformance-draft conformance-draft-server conformance-draft-client docs docs-guides docs-api docs-serve
|
.PHONY: deps-stable deps-low cs phpstan tests unit-tests inspector-tests coverage ci ci-stable ci-lowest conformance-tests conformance-server conformance-client docs
|
||||||
|
|
||||||
# The 2026-07-28 scenarios ship on the `alpha` dist-tag; `latest` (0.1.x) has
|
|
||||||
# none of them. Pinned to the same version CI runs (see
|
|
||||||
# .github/workflows/pipeline.yaml), so a local pass means a green pipeline.
|
|
||||||
CONFORMANCE_VERSION ?= 0.2.0-alpha.11
|
|
||||||
CONFORMANCE = npx --yes @modelcontextprotocol/conformance@$(CONFORMANCE_VERSION)
|
|
||||||
|
|
||||||
# The documentation toolchain is Python (Zensical, see requirements-docs.txt),
|
|
||||||
# run through uv so no virtualenv has to be managed by hand:
|
|
||||||
# https://docs.astral.sh/uv/getting-started/installation/
|
|
||||||
DOCS_RUN = uv run --no-project --with-requirements requirements-docs.txt --
|
|
||||||
|
|
||||||
deps-stable:
|
deps-stable:
|
||||||
composer update --prefer-stable
|
composer update --prefer-stable
|
||||||
|
|
@ -29,9 +18,6 @@ tests:
|
||||||
unit-tests:
|
unit-tests:
|
||||||
vendor/bin/phpunit --testsuite=unit
|
vendor/bin/phpunit --testsuite=unit
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
vendor/bin/phpunit --testsuite=integration
|
|
||||||
|
|
||||||
inspector-tests:
|
inspector-tests:
|
||||||
vendor/bin/phpunit --testsuite=inspector
|
vendor/bin/phpunit --testsuite=inspector
|
||||||
|
|
||||||
|
|
@ -42,32 +28,14 @@ conformance-server:
|
||||||
@echo "Waiting for server to start..."
|
@echo "Waiting for server to start..."
|
||||||
@sleep 5
|
@sleep 5
|
||||||
rm -rf tests/Conformance/results
|
rm -rf tests/Conformance/results
|
||||||
cd tests/Conformance && $(CONFORMANCE) server --url http://localhost:8000/ --suite all --spec-version 2025-11-25 --expected-failures conformance-baseline-2025-11-25.yml --output-dir results || true
|
cd tests/Conformance && npx @modelcontextprotocol/conformance server --url http://localhost:8000/ --output-dir results || true
|
||||||
php tests/Conformance/score.php server 2025-11-25
|
php tests/Conformance/score.php server
|
||||||
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
|
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
|
||||||
|
|
||||||
conformance-client:
|
conformance-client:
|
||||||
rm -rf tests/Conformance/results
|
rm -rf tests/Conformance/results
|
||||||
cd tests/Conformance && $(CONFORMANCE) client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --spec-version 2025-11-25 --expected-failures conformance-baseline-2025-11-25.yml --output-dir results || true
|
cd tests/Conformance && npx @modelcontextprotocol/conformance client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --expected-failures conformance-baseline.yml --output-dir results || true
|
||||||
php tests/Conformance/score.php client 2025-11-25
|
php tests/Conformance/score.php client
|
||||||
|
|
||||||
# --- 2026-07-28 (SEP-2575 stateless lifecycle) ------------------------------
|
|
||||||
|
|
||||||
conformance-draft: conformance-draft-server conformance-draft-client
|
|
||||||
|
|
||||||
conformance-draft-server:
|
|
||||||
docker compose -f tests/Conformance/Fixtures/docker-compose.yml up -d
|
|
||||||
@echo "Waiting for server to start..."
|
|
||||||
@sleep 5
|
|
||||||
rm -rf tests/Conformance/results-2026-07-28
|
|
||||||
cd tests/Conformance && $(CONFORMANCE) server --url http://localhost:8000/ --suite all --spec-version 2026-07-28 --expected-failures conformance-baseline-2026-07-28.yml --output-dir results-2026-07-28 || true
|
|
||||||
php tests/Conformance/score.php server 2026-07-28 results-2026-07-28
|
|
||||||
docker compose -f tests/Conformance/Fixtures/docker-compose.yml down
|
|
||||||
|
|
||||||
conformance-draft-client:
|
|
||||||
rm -rf tests/Conformance/results-2026-07-28
|
|
||||||
cd tests/Conformance && $(CONFORMANCE) client --command "php $(CURDIR)/tests/Conformance/client.php" --suite all --spec-version 2026-07-28 --expected-failures conformance-baseline-2026-07-28.yml --output-dir results-2026-07-28 || true
|
|
||||||
php tests/Conformance/score.php client 2026-07-28 results-2026-07-28
|
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage
|
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage
|
||||||
|
|
@ -78,19 +46,7 @@ ci-stable: deps-stable cs phpstan tests
|
||||||
|
|
||||||
ci-lowest: deps-low cs phpstan tests
|
ci-lowest: deps-low cs phpstan tests
|
||||||
|
|
||||||
# The published site is the guides (Zensical) with the phpDocumentor API
|
docs:
|
||||||
# reference mounted at /api/. `zensical build` wipes site/, so it runs first.
|
vendor/bin/phpdoc
|
||||||
docs: docs-guides docs-api
|
@grep -q 'No errors have been found' .phpdoc/build/reports/errors.html || \
|
||||||
rm -rf site/api
|
(echo "Documentation errors found. See build/docs/reports/errors.html" && exit 1)
|
||||||
cp -a .phpdoc/build/api site/api
|
|
||||||
|
|
||||||
docs-guides:
|
|
||||||
$(DOCS_RUN) zensical build --strict
|
|
||||||
|
|
||||||
docs-api:
|
|
||||||
vendor/bin/phpdoc --no-interaction
|
|
||||||
@grep -q 'No errors have been found' .phpdoc/build/api/reports/errors.html || \
|
|
||||||
(echo "Documentation errors found. See .phpdoc/build/api/reports/errors.html" && exit 1)
|
|
||||||
|
|
||||||
docs-serve:
|
|
||||||
$(DOCS_RUN) zensical serve
|
|
||||||
|
|
|
||||||
278
README.md
278
README.md
|
|
@ -1,13 +1,18 @@
|
||||||
# MCP PHP SDK
|
# MCP PHP SDK
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
[](https://packagist.org/packages/mcp/sdk)
|
[](https://packagist.org/packages/mcp/sdk)
|
||||||
[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
|
[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
|
||||||
[](https://packagist.org/packages/mcp/sdk)
|
[](https://packagist.org/packages/mcp/sdk)
|
||||||
[](LICENSE)
|
[](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 the Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP
|
</div>
|
||||||
servers and clients in PHP — tools, resources, prompts, STDIO and HTTP transports, sessions, authorization, and both
|
|
||||||
protocol eras (the `initialize` handshake and the stateless `2026-07-28` revision).
|
The official PHP SDK for Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP servers
|
||||||
|
and clients in PHP.
|
||||||
|
|
||||||
This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts
|
This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts
|
||||||
development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the
|
development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the
|
||||||
|
|
@ -16,16 +21,18 @@ development practices and standards from the Symfony project, including [Coding
|
||||||
Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html), please see the [roadmap](./ROADMAP.md) for
|
Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html), please see the [roadmap](./ROADMAP.md) for
|
||||||
planned next steps and features.
|
planned next steps and features.
|
||||||
|
|
||||||
## Protocol Conformance
|
## Table of Contents
|
||||||
|
|
||||||
Measured weekly against the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance),
|
- [Installation](#installation)
|
||||||
as passing checks out of total. The `2026-07-28` scores run against the framework's `alpha` releases, so they move as
|
- [Overview](#overview)
|
||||||
upstream publishes new scenarios.
|
- [Server SDK](#server-sdk)
|
||||||
|
- [Client SDK](#client-sdk)
|
||||||
| Revision | Server | Client |
|
- [Documentation](#documentation)
|
||||||
|:--------:|:------:|:------:|
|
- [External Resources](#external-resources)
|
||||||
| `2026-07-28` | [](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) | [](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) |
|
- [PHP Libraries Using the MCP SDK](#php-libraries-using-the-mcp-sdk)
|
||||||
| `2025-11-25` | [](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) | [](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml) |
|
- [Contributing](#contributing)
|
||||||
|
- [Credits](#credits)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -33,21 +40,28 @@ upstream publishes new scenarios.
|
||||||
composer require mcp/sdk
|
composer require mcp/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build a server
|
## Overview
|
||||||
|
|
||||||
A server is a plain PHP class plus three lines of wiring:
|
The MCP PHP SDK provides both **server** and **client** implementations for the Model Context Protocol, enabling you to:
|
||||||
|
|
||||||
|
- **Build MCP Servers**: Expose your PHP application's functionality (tools, resources, prompts) to AI agents
|
||||||
|
- **Build MCP Clients**: Connect to and interact with MCP servers from your PHP applications
|
||||||
|
|
||||||
|
## Server SDK
|
||||||
|
|
||||||
|
Build MCP servers to expose your PHP application's capabilities to AI agents like Claude, Codex, and others.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Mcp\Capability\Attribute\McpResource;
|
|
||||||
use Mcp\Capability\Attribute\McpTool;
|
|
||||||
use Mcp\Server;
|
use Mcp\Server;
|
||||||
use Mcp\Server\Transport\StdioTransport;
|
use Mcp\Server\Transport\StdioTransport;
|
||||||
|
use Mcp\Capability\Attribute\McpTool;
|
||||||
|
use Mcp\Capability\Attribute\McpResource;
|
||||||
|
|
||||||
class Calculator
|
// Define capabilities using PHP attributes
|
||||||
|
class CalculatorCapabilities
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Adds two numbers.
|
|
||||||
*/
|
|
||||||
#[McpTool]
|
#[McpTool]
|
||||||
public function add(int $a, int $b): int
|
public function add(int $a, int $b): int
|
||||||
{
|
{
|
||||||
|
|
@ -55,56 +69,228 @@ class Calculator
|
||||||
}
|
}
|
||||||
|
|
||||||
#[McpResource(uri: 'config://calculator/settings')]
|
#[McpResource(uri: 'config://calculator/settings')]
|
||||||
public function settings(): array
|
public function getSettings(): array
|
||||||
{
|
{
|
||||||
return ['precision' => 2];
|
return ['precision' => 2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit(Server::builder()
|
// Build and run the server
|
||||||
->setServerInfo('Calculator', '1.0.0')
|
$server = Server::builder()
|
||||||
->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
|
->setServerInfo('Calculator Server', '1.0.0')
|
||||||
->build()
|
->setDiscovery(__DIR__, ['.']) // Auto-discover attributes
|
||||||
->run(new StdioTransport()));
|
->build();
|
||||||
|
|
||||||
|
$transport = new StdioTransport();
|
||||||
|
$server->run($transport);
|
||||||
```
|
```
|
||||||
|
|
||||||
The walkthrough in [First server](docs/get-started/first-server.md) explains each piece, and
|
### Server Capabilities
|
||||||
[Try it with the Inspector](docs/get-started/inspector.md) shows it running.
|
|
||||||
|
|
||||||
## Build a client
|
- **Tools**: Executable functions that AI agents can call
|
||||||
|
- **Resources**: Data sources that can be read (files, configs, databases)
|
||||||
|
- **Resource Templates**: Dynamic resources with URI parameters
|
||||||
|
- **Prompts**: Pre-defined templates for AI interactions
|
||||||
|
- **Server-Initiated Communication**: Elicitations, sampling, logging, progress notifications
|
||||||
|
|
||||||
|
### Registration Methods
|
||||||
|
|
||||||
|
There are multiple ways to register your MCP capabilities—choose the approach that best fits your application's architecture:
|
||||||
|
|
||||||
|
**1. Attribute-Based Discovery** — Define capabilities using PHP attributes for automatic discovery:
|
||||||
|
```php
|
||||||
|
#[McpTool]
|
||||||
|
public function generateReport(): string { /* ... */ }
|
||||||
|
|
||||||
|
#[McpResource(uri: 'config://app/settings')]
|
||||||
|
public function getConfig(): array { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Manual Registration** — Register capabilities programmatically without attributes:
|
||||||
|
```php
|
||||||
|
$server = Server::builder()
|
||||||
|
->addTool([Calculator::class, 'add'], 'add_numbers')
|
||||||
|
->addResource([Config::class, 'get'], 'config://app')
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Hybrid Approach** — Combine both methods for maximum flexibility:
|
||||||
|
```php
|
||||||
|
$server = Server::builder()
|
||||||
|
->setDiscovery(__DIR__, ['.'])
|
||||||
|
->addTool([ExternalService::class, 'process'], 'external')
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transports
|
||||||
|
|
||||||
|
Choose the transport that matches your deployment environment:
|
||||||
|
|
||||||
|
**1. STDIO Transport** — For command-line integration and local processes:
|
||||||
|
```php
|
||||||
|
$transport = new StdioTransport();
|
||||||
|
$server->run($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. HTTP Transport** — For web-based servers and distributed systems:
|
||||||
|
```php
|
||||||
|
$transport = new StreamableHttpTransport($request, $responseFactory, $streamFactory);
|
||||||
|
$response = $server->run($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
Configure session storage to maintain state between requests. Choose the backend that fits your infrastructure:
|
||||||
|
|
||||||
|
**In-Memory** (default, suitable for STDIO):
|
||||||
|
```php
|
||||||
|
$server = Server::builder()
|
||||||
|
->setSession(ttl: 7200) // 2 hours
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
**File-Based** (suitable for single-server HTTP deployments):
|
||||||
|
```php
|
||||||
|
$server = Server::builder()
|
||||||
|
->setSession(new FileSessionStore(__DIR__ . '/sessions'))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
**PSR-16 Cache** (for example with Redis for scaled deployments):
|
||||||
|
```php
|
||||||
|
$server = Server::builder()
|
||||||
|
->setSession(new Psr16SessionStore(
|
||||||
|
cache: new Psr16Cache($redisAdapter),
|
||||||
|
prefix: 'mcp-',
|
||||||
|
ttl: 3600
|
||||||
|
))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
[→ Server Documentation](docs/server-builder.md)
|
||||||
|
|
||||||
|
## Client SDK
|
||||||
|
|
||||||
|
Connect to MCP servers from your PHP applications to access their tools, resources, and prompts.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Mcp\Client;
|
use Mcp\Client;
|
||||||
use Mcp\Client\Transport\StdioTransport;
|
use Mcp\Client\Transport\StdioTransport;
|
||||||
|
|
||||||
|
// Build the client
|
||||||
$client = Client::builder()
|
$client = Client::builder()
|
||||||
->setClientInfo('My Application', '1.0.0')
|
->setClientInfo('My Application', '1.0.0')
|
||||||
|
->setInitTimeout(30)
|
||||||
|
->setRequestTimeout(120)
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
$client->connect(new StdioTransport(command: 'php', args: ['/path/to/server.php']));
|
// Connect to a server
|
||||||
|
$transport = new StdioTransport(
|
||||||
|
command: 'php',
|
||||||
|
args: ['/path/to/server.php'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$client->connect($transport);
|
||||||
|
|
||||||
|
// Discover and use capabilities
|
||||||
$tools = $client->listTools();
|
$tools = $client->listTools();
|
||||||
$result = $client->callTool('add', ['a' => 5, 'b' => 3]);
|
$result = $client->callTool('add', ['a' => 5, 'b' => 3]);
|
||||||
|
|
||||||
|
$resources = $client->listResources();
|
||||||
|
$content = $client->readResource('config://calculator/settings');
|
||||||
|
|
||||||
$client->disconnect();
|
$client->disconnect();
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Connecting to a server](docs/client/connecting.md) for transports, timeouts, and the
|
### Client Capabilities
|
||||||
handlers that answer server-initiated requests.
|
|
||||||
|
- **Tool Calling**: List and execute tools from any MCP server
|
||||||
|
- **Resource Access**: Read static and dynamic resources
|
||||||
|
- **Prompt Management**: List and retrieve prompt templates
|
||||||
|
- **Completion Support**: Request argument completion suggestions
|
||||||
|
- **Sampling & Elicitation**: Respond to server-initiated LLM sampling and user-input requests
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
- **Progress Tracking**: Real-time progress during long operations
|
||||||
|
```php
|
||||||
|
$result = $client->callTool(
|
||||||
|
name: 'process_data',
|
||||||
|
arguments: ['dataset' => 'large_file.csv'],
|
||||||
|
onProgress: function (float $progress, ?float $total, ?string $message) {
|
||||||
|
echo "Progress: {$progress}/{$total} - {$message}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Sampling Support**: Handle server LLM sampling requests
|
||||||
|
```php
|
||||||
|
$samplingHandler = new SamplingRequestHandler($myCallback);
|
||||||
|
$client = Client::builder()
|
||||||
|
->setCapabilities(new ClientCapabilities(sampling: true))
|
||||||
|
->addRequestHandler($samplingHandler)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Elicitation Support**: Respond to server requests for user input
|
||||||
|
```php
|
||||||
|
$elicitationHandler = new ElicitationRequestHandler($myCallback);
|
||||||
|
$client = Client::builder()
|
||||||
|
->setCapabilities(new ClientCapabilities(elicitation: true))
|
||||||
|
->addRequestHandler($elicitationHandler)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Logging Notifications**: Receive server log messages
|
||||||
|
```php
|
||||||
|
$loggingHandler = new LoggingNotificationHandler($myCallback);
|
||||||
|
$client = Client::builder()
|
||||||
|
->addNotificationHandler($loggingHandler)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transports
|
||||||
|
|
||||||
|
Connect to MCP servers using the transport that matches your setup:
|
||||||
|
|
||||||
|
**1. STDIO Transport** — Connect to local server processes:
|
||||||
|
```php
|
||||||
|
$transport = new StdioTransport(
|
||||||
|
command: 'php',
|
||||||
|
args: ['/path/to/server.php'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$client->connect($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. HTTP Transport** — Connect to remote or web-based servers:
|
||||||
|
```php
|
||||||
|
$transport = new HttpTransport('http://localhost:8000');
|
||||||
|
|
||||||
|
$client->connect($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
[→ Client Documentation](docs/client.md)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The full documentation is published at **[php.sdk.modelcontextprotocol.io](https://php.sdk.modelcontextprotocol.io/)**.
|
### Core Concepts
|
||||||
|
|
||||||
- **[Get started](docs/get-started/index.md)** — Install the SDK and build your first server
|
- **[Server Builder](docs/server-builder.md)** — Complete ServerBuilder reference and configuration
|
||||||
- **[Servers](docs/servers/index.md)** — Tools, resources, resource templates, prompts, and how to register them
|
- **[Client](docs/client.md)** — Client SDK for connecting to and communicating with MCP servers
|
||||||
- **[Inside your handler](docs/handlers/index.md)** — Talking back to the client, logging, and asking for input
|
- **[Transports](docs/transports.md)** — STDIO and HTTP transport setup and usage
|
||||||
- **[Running your server](docs/run/index.md)** — Server builder, STDIO and HTTP transports, framework integration, sessions, authorization
|
- **[MCP Elements](docs/mcp-elements.md)** — Creating tools, resources, prompts, and templates
|
||||||
- **[Clients](docs/client/index.md)** — Client SDK for connecting to and communicating with MCP servers
|
- **[Server-Client Communication](docs/server-client-communication.md)** — Sampling, logging, progress, and notifications
|
||||||
- **[Protocol versions](docs/protocol-versions.md)** — The two protocol eras, and what revision `2026-07-28` changed
|
- **[Protocol Extensions](docs/extensions.md)** — Opt-in protocol extensions announced during capability negotiation, including MCP Apps (HTML UI resources)
|
||||||
- **[Advanced](docs/advanced/index.md)** — Events, protocol extensions (including MCP Apps), and custom message handlers
|
- **[Authorization](docs/authorization.md)** — OAuth and authorization setup for HTTP transport
|
||||||
- **[Examples](docs/examples.md)** — Runnable server and client examples
|
- **[Events](docs/events.md)** — Hooking into server lifecycle with events
|
||||||
- **[API Reference](https://php.sdk.modelcontextprotocol.io/api/)** — Generated class reference
|
|
||||||
|
### Learning & Examples
|
||||||
|
|
||||||
|
- **[Examples](docs/examples.md)** — Comprehensive example walkthroughs for servers and clients
|
||||||
|
- **[ROADMAP.md](ROADMAP.md)** — Planned features and development roadmap
|
||||||
|
|
||||||
## External Resources
|
## External Resources
|
||||||
|
|
||||||
|
|
@ -126,11 +312,9 @@ Building something on top of the SDK? Open a pull request to add it to this list
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We are passionate about supporting contributors of all levels of experience and would love to see you get involved in
|
We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project.
|
||||||
the project. Start by [reporting issues](https://github.com/modelcontextprotocol/php-sdk/issues) or
|
|
||||||
[sending pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls). See
|
See the [Contributing Guide](CONTRIBUTING.md) to get started before you [report issues](https://github.com/modelcontextprotocol/php-sdk/issues) and [send pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls).
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding standards, and what to run before
|
|
||||||
opening a PR.
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,6 @@ This roadmap is a living document that outlines the planned features and improve
|
||||||
- **Client**
|
- **Client**
|
||||||
- [x] Implement client-side support
|
- [x] Implement client-side support
|
||||||
- [x] Implement client examples and documentation
|
- [x] Implement client examples and documentation
|
||||||
- [ ] Implement OAuth2 authentication for client
|
|
||||||
- **Schema**
|
- **Schema**
|
||||||
- [ ] Implement schema generation based on TS or JSON Schema
|
- [ ] Implement schema generation based on TS or JSON Schema
|
||||||
- [ ] Achieve full spec conformance
|
|
||||||
- **Extensions**
|
|
||||||
- [ ] Implement the Tasks extension
|
|
||||||
- **Architecture**
|
|
||||||
- [ ] Split the SDK into multiple packages
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,5 +93,5 @@ If you need an authorization server (token issuance, client registration, login,
|
||||||
validator seams. The MCP server validates the tokens it issues; it does not issue them
|
validator seams. The MCP server validates the tokens it issues; it does not issue them
|
||||||
itself.
|
itself.
|
||||||
|
|
||||||
See [`../docs/run/authorization.md`](../docs/run/authorization.md) for the supported Resource Server
|
See [`../docs/authorization.md`](../docs/authorization.md) for the supported Resource Server
|
||||||
and delegation setup.
|
and delegation setup.
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
"psr/http-server-handler": "^1.0",
|
"psr/http-server-handler": "^1.0",
|
||||||
"psr/http-server-middleware": "^1.0",
|
"psr/http-server-middleware": "^1.0",
|
||||||
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
"symfony/deprecation-contracts": "^2.5 || ^3.0",
|
|
||||||
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
|
"symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path fill="none" d="M23.5996 85.2532L86.2021 22.6507C94.8457 14.0071 108.86 14.0071 117.503 22.6507C126.147 31.2942 126.147 45.3083 117.503 53.9519L70.2254 101.23" stroke="currentColor" stroke-width="11.0667" stroke-linecap="round"/><path fill="none" d="M70.8789 100.578L117.504 53.952C126.148 45.3083 140.163 45.3083 148.806 53.952L149.132 54.278C157.776 62.9216 157.776 76.9357 149.132 85.5792L92.5139 142.198C89.6327 145.079 89.6327 149.75 92.5139 152.631L104.14 164.257" stroke="currentColor" stroke-width="11.0667" stroke-linecap="round"/><path fill="none" d="M101.853 38.3013L55.553 84.6011C46.9094 93.2447 46.9094 107.258 55.553 115.902C64.1966 124.546 78.2106 124.546 86.8543 115.902L133.154 69.6025" stroke="currentColor" stroke-width="11.0667" stroke-linecap="round"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 848 B |
|
|
@ -1 +0,0 @@
|
||||||
php.sdk.modelcontextprotocol.io
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
# Custom Message Handlers
|
|
||||||
|
|
||||||
**Low-level escape hatch.** Custom message handlers run before the SDK's built-in handlers and give you total control over
|
|
||||||
individual JSON-RPC messages. They do not receive the builder's registry, container, or discovery output unless you pass
|
|
||||||
those dependencies in yourself.
|
|
||||||
|
|
||||||
> **Warning**: Custom message handlers bypass discovery, manual capability registration, and container lookups (unless
|
|
||||||
> you explicitly pass them). Tools, resources, and prompts you register elsewhere will not show up unless your handler
|
|
||||||
> loads and executes them manually. Reach for this API only when you need that level of control and are comfortable
|
|
||||||
> taking on the additional plumbing.
|
|
||||||
|
|
||||||
## Request Handlers
|
|
||||||
|
|
||||||
Handle JSON-RPC requests (messages with an `id` that expect a response). Request handlers **must** return either a
|
|
||||||
`Response` or an `Error` object.
|
|
||||||
|
|
||||||
Attach request handlers with `addRequestHandler()` (single) or `addRequestHandlers()` (multiple). You can call these
|
|
||||||
methods as many times as needed; handlers are collected in call order, and the whole custom list runs before the
|
|
||||||
built-in defaults:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$server = Server::builder()
|
|
||||||
->addRequestHandler(new CustomListToolsHandler())
|
|
||||||
->addRequestHandlers([
|
|
||||||
new CustomCallToolHandler(),
|
|
||||||
new CustomGetPromptHandler(),
|
|
||||||
])
|
|
||||||
->build();
|
|
||||||
```
|
|
||||||
|
|
||||||
Request handlers implement `RequestHandlerInterface`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
use Mcp\Schema\JsonRpc\Error;
|
|
||||||
use Mcp\Schema\JsonRpc\Request;
|
|
||||||
use Mcp\Schema\JsonRpc\Response;
|
|
||||||
use Mcp\Server\Handler\Request\RequestHandlerInterface;
|
|
||||||
use Mcp\Server\Session\SessionInterface;
|
|
||||||
|
|
||||||
interface RequestHandlerInterface
|
|
||||||
{
|
|
||||||
public function supports(Request $request): bool;
|
|
||||||
|
|
||||||
public function handle(Request $request, SessionInterface $session): Response|Error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `supports()` decides if the handler should process the incoming request
|
|
||||||
- `handle()` **must** return a `Response` (on success) or an `Error` (on failure)
|
|
||||||
|
|
||||||
## Notification Handlers
|
|
||||||
|
|
||||||
Handle JSON-RPC notifications (messages without an `id` that don't expect a response). Notification handlers **do not**
|
|
||||||
return anything - they perform side effects only.
|
|
||||||
|
|
||||||
Attach notification handlers with `addNotificationHandler()` (single) or `addNotificationHandlers()` (multiple):
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Handlers are your own classes implementing NotificationHandlerInterface;
|
|
||||||
// the SDK ships only its internal ones.
|
|
||||||
$server = Server::builder()
|
|
||||||
->addNotificationHandler(new AuditNotificationHandler($auditLog))
|
|
||||||
->addNotificationHandlers([
|
|
||||||
new MetricsNotificationHandler($metrics),
|
|
||||||
new CancellationNotificationHandler(),
|
|
||||||
])
|
|
||||||
->build();
|
|
||||||
```
|
|
||||||
|
|
||||||
Notification handlers implement `NotificationHandlerInterface`:
|
|
||||||
|
|
||||||
```php
|
|
||||||
use Mcp\Schema\JsonRpc\Notification;
|
|
||||||
use Mcp\Server\Handler\Notification\NotificationHandlerInterface;
|
|
||||||
use Mcp\Server\Session\SessionInterface;
|
|
||||||
|
|
||||||
interface NotificationHandlerInterface
|
|
||||||
{
|
|
||||||
public function supports(Notification $notification): bool;
|
|
||||||
|
|
||||||
public function handle(Notification $notification, SessionInterface $session): void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `supports()` decides if the handler should process the incoming notification
|
|
||||||
- `handle()` performs side effects but **does not** return a value (notifications have no response)
|
|
||||||
|
|
||||||
## Key Differences
|
|
||||||
|
|
||||||
| Handler Type | Interface | Returns | Use Case |
|
|
||||||
|-------------|-----------|---------|----------|
|
|
||||||
| Request Handler | `RequestHandlerInterface` | `Response\|Error` | Handle requests that need responses (e.g., `tools/list`, `tools/call`) |
|
|
||||||
| Notification Handler | `NotificationHandlerInterface` | `void` | Handle fire-and-forget notifications (e.g., `notifications/initialized`, `notifications/progress`) |
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Check out [`examples/server/custom-method-handlers/server.php`](https://github.com/modelcontextprotocol/php-sdk/blob/main/examples/server/custom-method-handlers/server.php) for a complete example showing how to implement
|
|
||||||
custom `tools/list` and `tools/call` request handlers independently of the registry.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Advanced
|
|
||||||
|
|
||||||
Everything here is optional. A working server needs none of it.
|
|
||||||
|
|
||||||
* **[Events](events.md)** — PSR-14 events dispatched around every request, response,
|
|
||||||
error, and notification. Useful for metrics, audit logs, and debugging.
|
|
||||||
* **[Protocol extensions](extensions.md)** — opt-in extensions announced during
|
|
||||||
capability negotiation, including MCP Apps (HTML UI resources).
|
|
||||||
* **[Custom message handlers](custom-handlers.md)** — taking over a JSON-RPC method the
|
|
||||||
SDK does not implement, or overriding one it does.
|
|
||||||
448
docs/authorization.md
Normal file
448
docs/authorization.md
Normal file
|
|
@ -0,0 +1,448 @@
|
||||||
|
# Authorization
|
||||||
|
|
||||||
|
The PHP MCP SDK provides OAuth 2.1 authorization support for HTTP transports, implementing the
|
||||||
|
[MCP Authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Scope: what this SDK does and does not do](#scope-what-this-sdk-does-and-does-not-do)
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Components](#components)
|
||||||
|
- [JWT Token Validation](#jwt-token-validation)
|
||||||
|
- [Protected Resource Metadata](#protected-resource-metadata)
|
||||||
|
- [Custom Token Validators](#custom-token-validators)
|
||||||
|
- [Scope-Based Access Control](#scope-based-access-control)
|
||||||
|
- [Examples](#examples)
|
||||||
|
|
||||||
|
## Scope: what this SDK does and does not do
|
||||||
|
|
||||||
|
The MCP server is an OAuth 2.1 **Resource Server**. It validates the tokens it receives and may
|
||||||
|
delegate the OAuth flow to your upstream Identity Provider. **It is not an authorization server
|
||||||
|
and it does not issue tokens.**
|
||||||
|
|
||||||
|
| Role | What it does | Status | Scope |
|
||||||
|
|------|--------------|--------|-------|
|
||||||
|
| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Supported (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **In scope** |
|
||||||
|
| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Supported (`OAuthProxyMiddleware`) | **In scope — delegation only** |
|
||||||
|
| Authorization Server / Identity Provider | Mints its own tokens, registers clients, runs login and consent | Not implemented | **Out of scope: being an authorization server / issuing tokens is out of scope** |
|
||||||
|
|
||||||
|
To issue tokens, front the MCP server with an external IdP (Keycloak, Auth0, Microsoft Entra
|
||||||
|
ID, Okta) or run `league/oauth2-server` in your own application, and let the MCP server
|
||||||
|
validate those tokens as a Resource Server. See
|
||||||
|
[adr/0001-oauth-authorization-server-out-of-scope.md](../adr/0001-oauth-authorization-server-out-of-scope.md).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Authorization in MCP is implemented at the transport level using PSR-15 middleware. The SDK provides:
|
||||||
|
|
||||||
|
- **AuthorizationMiddleware** - PSR-15 middleware that enforces bearer token authentication
|
||||||
|
- **ProtectedResourceMetadataMiddleware** - Serves RFC 9728 metadata at well-known endpoints
|
||||||
|
- **OAuthProxyMiddleware** - Delegates OAuth flows (`/authorize`, `/token`) to your upstream IdP; the SDK never issues tokens itself
|
||||||
|
- **OAuthRequestMetaMiddleware** - Bridges HTTP OAuth attributes to JSON-RPC request meta
|
||||||
|
- **JwtTokenValidator** - Validates JWT tokens using JWKS from OAuth 2.0 / OIDC providers
|
||||||
|
- **OidcDiscovery** - Discovers authorization server metadata from well-known endpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌────────────────────┐ ┌─────────────────┐
|
||||||
|
│ MCP Client │────▶│ AuthorizationMiddleware │────▶│ MCP Handlers │
|
||||||
|
└─────────────┘ └────────────────────┘ └─────────────────┘
|
||||||
|
│ │
|
||||||
|
│ │ Validate JWT
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────┐ ┌─────────────────┐
|
||||||
|
│ Auth Server │◀────│ JwtTokenValidator│
|
||||||
|
│ (Keycloak, │ │ + JWKS │
|
||||||
|
│ Entra ID) │ └─────────────────┘
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Server;
|
||||||
|
use Mcp\Server\Transport\Http\Middleware\AuthorizationMiddleware;
|
||||||
|
use Mcp\Server\Transport\Http\Middleware\ProtectedResourceMetadataMiddleware;
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\JwksProvider;
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\JwtTokenValidator;
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\OidcDiscovery;
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\ProtectedResourceMetadata;
|
||||||
|
use Mcp\Server\Transport\StreamableHttpTransport;
|
||||||
|
|
||||||
|
// 1. Set up OIDC discovery and JWKS provider
|
||||||
|
$discovery = new OidcDiscovery();
|
||||||
|
$jwksProvider = new JwksProvider($discovery);
|
||||||
|
|
||||||
|
// 2. Create JWT validator for your OAuth provider
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: 'https://auth.example.com/realms/mcp',
|
||||||
|
audience: 'mcp-server',
|
||||||
|
jwksProvider: $jwksProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Create Protected Resource Metadata (RFC 9728)
|
||||||
|
$metadata = new ProtectedResourceMetadata(
|
||||||
|
authorizationServers: ['https://auth.example.com/realms/mcp'],
|
||||||
|
scopesSupported: ['mcp:read', 'mcp:write'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Create middleware stack
|
||||||
|
$authMiddleware = new AuthorizationMiddleware(
|
||||||
|
validator: $validator,
|
||||||
|
resourceMetadata: $metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
$metadataMiddleware = new ProtectedResourceMetadataMiddleware(
|
||||||
|
metadata: $metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Create transport with middleware
|
||||||
|
$transport = new StreamableHttpTransport(
|
||||||
|
$request,
|
||||||
|
middlewares: [$metadataMiddleware, $authMiddleware],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Run server
|
||||||
|
$server = Server::builder()
|
||||||
|
->setServerInfo('Protected MCP Server', '1.0.0')
|
||||||
|
->setDiscovery(__DIR__)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
$response = $server->run($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### AuthorizationMiddleware
|
||||||
|
|
||||||
|
The main middleware that enforces authentication:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$middleware = new AuthorizationMiddleware(
|
||||||
|
validator: $validator, // AuthorizationTokenValidatorInterface
|
||||||
|
resourceMetadata: $metadata, // ProtectedResourceMetadata instance
|
||||||
|
responseFactory: null, // PSR-17 (auto-discovered)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
|
||||||
|
| Request | Response |
|
||||||
|
|---------|----------|
|
||||||
|
| Missing Authorization header | 401 with `WWW-Authenticate: Bearer resource_metadata="..."` |
|
||||||
|
| Invalid/expired token | 401 with error details |
|
||||||
|
| Valid token | Passes to next handler with OAuth attributes on request |
|
||||||
|
|
||||||
|
### ProtectedResourceMetadataMiddleware
|
||||||
|
|
||||||
|
Serves Protected Resource Metadata at configured well-known paths:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$metadataMiddleware = new ProtectedResourceMetadataMiddleware(
|
||||||
|
metadata: $metadata, // ProtectedResourceMetadata instance
|
||||||
|
responseFactory: null, // PSR-17 (auto-discovered)
|
||||||
|
streamFactory: null, // PSR-17 (auto-discovered)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### JwtTokenValidator
|
||||||
|
|
||||||
|
Validates JWT access tokens:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: 'https://auth.example.com', // Expected issuer claim
|
||||||
|
audience: 'mcp-server', // Expected audience (string or array)
|
||||||
|
jwksProvider: $jwksProvider, // JwksProviderInterface
|
||||||
|
jwksUri: null, // Explicit JWKS URI (auto-discovered)
|
||||||
|
algorithms: ['RS256', 'RS384'], // Allowed algorithms
|
||||||
|
scopeClaim: 'scope', // Claim name for scopes
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Attributes:**
|
||||||
|
|
||||||
|
After successful validation, these attributes are added to the request:
|
||||||
|
|
||||||
|
| Attribute | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `oauth.claims` | All JWT claims as array |
|
||||||
|
| `oauth.scopes` | Extracted scopes as array |
|
||||||
|
| `oauth.subject` | The `sub` claim |
|
||||||
|
| `oauth.client_id` | The `client_id` claim (if present) |
|
||||||
|
| `oauth.authorized_party` | The `azp` claim (if present) |
|
||||||
|
|
||||||
|
### ProtectedResourceMetadata
|
||||||
|
|
||||||
|
Represents RFC 9728 Protected Resource Metadata:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$metadata = new ProtectedResourceMetadata(
|
||||||
|
authorizationServers: [ // Required: authorization server URLs
|
||||||
|
'https://auth.example.com',
|
||||||
|
],
|
||||||
|
scopesSupported: [ // Optional: supported scopes
|
||||||
|
'mcp:read',
|
||||||
|
'mcp:write',
|
||||||
|
],
|
||||||
|
resource: 'https://mcp.example.com', // Optional: resource identifier
|
||||||
|
resourceName: 'My MCP Server', // Optional: human-readable name
|
||||||
|
metadataPaths: [ // Paths to serve metadata (default: /.well-known/oauth-protected-resource)
|
||||||
|
'/.well-known/oauth-protected-resource',
|
||||||
|
],
|
||||||
|
extra: [ // Optional: additional fields
|
||||||
|
'custom_field' => 'value',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### OidcDiscovery
|
||||||
|
|
||||||
|
Discovers OAuth/OIDC server metadata:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$discovery = new OidcDiscovery(
|
||||||
|
httpClient: null, // PSR-18 (auto-discovered)
|
||||||
|
requestFactory: null, // PSR-17 (auto-discovered)
|
||||||
|
cache: $cache, // PSR-16 cache (optional)
|
||||||
|
cacheTtl: 3600, // Cache TTL
|
||||||
|
);
|
||||||
|
|
||||||
|
// Discover metadata
|
||||||
|
$metadata = $discovery->discover('https://auth.example.com/realms/mcp');
|
||||||
|
|
||||||
|
// Get specific endpoints
|
||||||
|
$jwksUri = $discovery->getJwksUri($issuer);
|
||||||
|
$tokenEndpoint = $discovery->getTokenEndpoint($issuer);
|
||||||
|
$authEndpoint = $discovery->getAuthorizationEndpoint($issuer);
|
||||||
|
```
|
||||||
|
|
||||||
|
### JwksProvider
|
||||||
|
|
||||||
|
Fetches and caches JWKS key sets:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$jwksProvider = new JwksProvider(
|
||||||
|
discovery: $discovery, // OidcDiscoveryInterface
|
||||||
|
httpClient: null, // PSR-18 (auto-discovered)
|
||||||
|
requestFactory: null, // PSR-17 (auto-discovered)
|
||||||
|
cache: $cache, // PSR-16 cache (optional)
|
||||||
|
cacheTtl: 3600, // JWKS cache TTL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Token Validation
|
||||||
|
|
||||||
|
### Keycloak
|
||||||
|
|
||||||
|
```php
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: 'https://keycloak.example.com/realms/mcp',
|
||||||
|
audience: 'mcp-server',
|
||||||
|
jwksProvider: $jwksProvider,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Microsoft Entra ID (Azure AD)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$tenantId = 'your-tenant-id';
|
||||||
|
$clientId = 'your-client-id';
|
||||||
|
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: "https://login.microsoftonline.com/{$tenantId}/v2.0",
|
||||||
|
audience: $clientId,
|
||||||
|
jwksProvider: $jwksProvider,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth0
|
||||||
|
|
||||||
|
```php
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: 'https://your-tenant.auth0.com/',
|
||||||
|
audience: 'https://api.example.com',
|
||||||
|
jwksProvider: $jwksProvider,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Okta
|
||||||
|
|
||||||
|
```php
|
||||||
|
$validator = new JwtTokenValidator(
|
||||||
|
issuer: 'https://your-org.okta.com/oauth2/default',
|
||||||
|
audience: 'api://default',
|
||||||
|
jwksProvider: $jwksProvider,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protected Resource Metadata
|
||||||
|
|
||||||
|
The `ProtectedResourceMetadataMiddleware` serves Protected Resource Metadata at configured paths, enabling clients to discover the authorization server:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"authorization_servers": ["https://auth.example.com/realms/mcp"],
|
||||||
|
"scopes_supported": ["mcp:read", "mcp:write"],
|
||||||
|
"resource": "https://mcp.example.com/mcp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients request this from `/.well-known/oauth-protected-resource` before authenticating.
|
||||||
|
|
||||||
|
### WWW-Authenticate Header
|
||||||
|
|
||||||
|
On 401 responses, the middleware includes:
|
||||||
|
|
||||||
|
```
|
||||||
|
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
|
||||||
|
scope="mcp:read mcp:write"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Token Validators
|
||||||
|
|
||||||
|
Implement `AuthorizationTokenValidatorInterface` for custom validation:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\AuthorizationTokenValidatorInterface;
|
||||||
|
use Mcp\Server\Transport\Http\OAuth\AuthorizationResult;
|
||||||
|
|
||||||
|
final class ApiKeyValidator implements AuthorizationTokenValidatorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private array $validKeys,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate(string $accessToken): AuthorizationResult
|
||||||
|
{
|
||||||
|
if (!isset($this->validKeys[$accessToken])) {
|
||||||
|
return AuthorizationResult::unauthorized(
|
||||||
|
'invalid_token',
|
||||||
|
'Unknown API key'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyInfo = $this->validKeys[$accessToken];
|
||||||
|
|
||||||
|
return AuthorizationResult::allow([
|
||||||
|
'api_key.name' => $keyInfo['name'],
|
||||||
|
'api_key.scopes' => $keyInfo['scopes'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
$validator = new ApiKeyValidator([
|
||||||
|
'sk_live_abc123' => ['name' => 'Production', 'scopes' => ['read', 'write']],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### AuthorizationResult
|
||||||
|
|
||||||
|
Factory methods for different outcomes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Allow access with attributes
|
||||||
|
AuthorizationResult::allow(['user_id' => '123']);
|
||||||
|
|
||||||
|
// Deny - missing/invalid token (401)
|
||||||
|
AuthorizationResult::unauthorized('invalid_token', 'Token expired');
|
||||||
|
|
||||||
|
// Deny - valid token but insufficient permissions (403)
|
||||||
|
AuthorizationResult::forbidden('insufficient_scope', 'Requires admin scope', ['admin']);
|
||||||
|
|
||||||
|
// Deny - malformed request (400)
|
||||||
|
AuthorizationResult::badRequest('invalid_request', 'Malformed header');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope-Based Access Control
|
||||||
|
|
||||||
|
### Checking Scopes in Handlers
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[McpTool(name: 'admin_action')]
|
||||||
|
public function adminAction(RequestContext $context): array
|
||||||
|
{
|
||||||
|
$scopes = $context->getRequest()?->getAttribute('oauth.scopes') ?? [];
|
||||||
|
|
||||||
|
if (!in_array('mcp:admin', $scopes, true)) {
|
||||||
|
throw new \RuntimeException('Admin scope required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform admin action
|
||||||
|
return ['status' => 'success'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using JwtTokenValidator::requireScopes
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In a custom middleware or handler
|
||||||
|
$result = $validator->validate($token);
|
||||||
|
|
||||||
|
if ($result->isAllowed()) {
|
||||||
|
// Check for specific scopes
|
||||||
|
$result = $validator->requireScopes($result, ['mcp:write']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$result->isAllowed()) {
|
||||||
|
// Handle insufficient scope (returns 403)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Complete working examples are available in the `examples/server/` directory:
|
||||||
|
|
||||||
|
### Keycloak Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/server/oauth-keycloak
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Test credentials: demo / demo123
|
||||||
|
```
|
||||||
|
|
||||||
|
See [oauth-keycloak/README.md](../examples/server/oauth-keycloak/README.md)
|
||||||
|
|
||||||
|
### Microsoft Entra ID Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/server/oauth-microsoft
|
||||||
|
cp env.example .env
|
||||||
|
# Edit .env with your Azure credentials
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
See [oauth-microsoft/README.md](../examples/server/oauth-microsoft/README.md)
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Always use HTTPS** in production for token transmission
|
||||||
|
2. **Validate audience claims** to prevent token confusion attacks
|
||||||
|
3. **Use short-lived tokens** and implement token refresh
|
||||||
|
4. **Cache JWKS** to reduce latency but allow for key rotation
|
||||||
|
5. **Never log tokens** - log only non-sensitive claims like subject
|
||||||
|
6. **Validate scopes** before performing sensitive operations
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Invalid issuer" error
|
||||||
|
|
||||||
|
The `iss` claim in the token must exactly match the configured issuer URL, including trailing slashes.
|
||||||
|
|
||||||
|
### "Invalid audience" error
|
||||||
|
|
||||||
|
Check the `aud` claim matches your configured audience. Some providers use the client ID, others use a custom URI.
|
||||||
|
|
||||||
|
### JWKS fetch timeout
|
||||||
|
|
||||||
|
- Ensure network connectivity to the authorization server
|
||||||
|
- Consider using a cache to reduce dependency on the auth server
|
||||||
|
- Check firewall rules allow outbound HTTPS
|
||||||
|
|
||||||
|
### Token expired
|
||||||
|
|
||||||
|
- Check clock synchronization between servers
|
||||||
|
- Tokens typically have a 5-minute clock skew tolerance
|
||||||
|
- Ensure clients refresh tokens before expiration
|
||||||
757
docs/client.md
Normal file
757
docs/client.md
Normal file
|
|
@ -0,0 +1,757 @@
|
||||||
|
# Client
|
||||||
|
|
||||||
|
The MCP Client SDK provides a synchronous, framework-agnostic API for communicating with MCP servers from PHP applications.
|
||||||
|
It handles connection management, request/response correlation, server-initiated requests (sampling), and real-time notifications.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Client Builder](#client-builder)
|
||||||
|
- [Transports](#transports)
|
||||||
|
- [Connecting to Servers](#connecting-to-servers)
|
||||||
|
- [Server Information](#server-information)
|
||||||
|
- [Working with Tools](#working-with-tools)
|
||||||
|
- [Working with Resources](#working-with-resources)
|
||||||
|
- [Working with Prompts](#working-with-prompts)
|
||||||
|
- [Server-Initiated Communication](#server-initiated-communication)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Complete Example](#complete-example)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The client follows a builder pattern for configuration and provides a synchronous API for all operations:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client;
|
||||||
|
use Mcp\Client\Transport\StdioTransport;
|
||||||
|
|
||||||
|
// Build and configure the client
|
||||||
|
$client = Client::builder()
|
||||||
|
->setClientInfo('My Client', '1.0.0')
|
||||||
|
->setInitTimeout(30)
|
||||||
|
->setRequestTimeout(120)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
// Create a transport
|
||||||
|
$transport = new StdioTransport(
|
||||||
|
command: 'php',
|
||||||
|
args: ['/path/to/server.php'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connect and use the server
|
||||||
|
$client->connect($transport);
|
||||||
|
$tools = $client->listTools();
|
||||||
|
$client->disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Builder
|
||||||
|
|
||||||
|
The `Client\Builder` provides fluent configuration of client instances.
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client;
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setClientInfo('My Application', '1.0.0', 'Description of my client')
|
||||||
|
->setInitTimeout(30) // Seconds to wait for initialization
|
||||||
|
->setRequestTimeout(120) // Seconds to wait for request responses
|
||||||
|
->setMaxRetries(3) // Retry attempts for failed connections
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Information
|
||||||
|
|
||||||
|
Set the client's identity reported to servers during initialization:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$client = Client::builder()
|
||||||
|
->setClientInfo(
|
||||||
|
name: 'AI Assistant Client',
|
||||||
|
version: '2.1.0',
|
||||||
|
description: 'Client for automated AI workflows'
|
||||||
|
)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protocol Version
|
||||||
|
|
||||||
|
Specify the MCP protocol version (defaults to latest):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Schema\Enum\ProtocolVersion;
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setProtocolVersion(ProtocolVersion::V2025_11_25)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
Declare client capabilities to enable server features:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Schema\ClientCapabilities;
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setCapabilities(new ClientCapabilities(
|
||||||
|
sampling: true, // Enable LLM sampling requests from server
|
||||||
|
roots: true, // Enable filesystem root listing
|
||||||
|
))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification Handlers
|
||||||
|
|
||||||
|
Register handlers for server-initiated notifications:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
|
||||||
|
use Mcp\Schema\Notification\LoggingMessageNotification;
|
||||||
|
|
||||||
|
$loggingHandler = new LoggingNotificationHandler(
|
||||||
|
static function (LoggingMessageNotification $notification) {
|
||||||
|
echo "[{$notification->level->value}] {$notification->data}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->addNotificationHandler($loggingHandler)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Handlers
|
||||||
|
|
||||||
|
Register handlers for server-initiated requests (e.g., sampling):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Handler\Request\SamplingRequestHandler;
|
||||||
|
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
|
||||||
|
use Mcp\Schema\Request\CreateSamplingMessageRequest;
|
||||||
|
use Mcp\Schema\Result\CreateSamplingMessageResult;
|
||||||
|
|
||||||
|
$samplingCallback = new class implements SamplingCallbackInterface {
|
||||||
|
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
|
||||||
|
{
|
||||||
|
// Perform LLM sampling and return result
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->addRequestHandler(new SamplingRequestHandler($samplingCallback))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logger
|
||||||
|
|
||||||
|
Configure PSR-3 logging for debugging:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
|
||||||
|
$logger = new Logger('mcp-client');
|
||||||
|
$logger->pushHandler(new StreamHandler('client.log', Logger::DEBUG));
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setLogger($logger)
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transports
|
||||||
|
|
||||||
|
Transports handle the communication layer between client and server.
|
||||||
|
|
||||||
|
### STDIO Transport
|
||||||
|
|
||||||
|
Spawns a server process and communicates via standard input/output:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Transport\StdioTransport;
|
||||||
|
|
||||||
|
$transport = new StdioTransport(
|
||||||
|
command: 'php',
|
||||||
|
args: ['/path/to/server.php'],
|
||||||
|
cwd: '/working/directory', // Optional working directory
|
||||||
|
env: ['KEY' => 'value'], // Optional environment variables
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `command` (string): The command to execute
|
||||||
|
- `args` (array): Command arguments
|
||||||
|
- `cwd` (string|null): Working directory for the process
|
||||||
|
- `env` (array|null): Environment variables
|
||||||
|
- `logger` (LoggerInterface|null): Optional PSR-3 logger
|
||||||
|
|
||||||
|
### HTTP Transport
|
||||||
|
|
||||||
|
Communicates with remote MCP servers over HTTP:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Transport\HttpTransport;
|
||||||
|
|
||||||
|
$transport = new HttpTransport(
|
||||||
|
endpoint: 'http://localhost:8000',
|
||||||
|
headers: ['Authorization' => 'Bearer token'],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `endpoint` (string): The MCP server URL
|
||||||
|
- `headers` (array): Additional HTTP headers
|
||||||
|
- `httpClient` (ClientInterface|null): PSR-18 HTTP client (auto-discovered)
|
||||||
|
- `requestFactory` (RequestFactoryInterface|null): PSR-17 request factory (auto-discovered)
|
||||||
|
- `streamFactory` (StreamFactoryInterface|null): PSR-17 stream factory (auto-discovered)
|
||||||
|
- `logger` (LoggerInterface|null): Optional PSR-3 logger
|
||||||
|
|
||||||
|
**PSR-18 Auto-Discovery:**
|
||||||
|
|
||||||
|
The transport automatically discovers PSR-18 HTTP clients from:
|
||||||
|
- `php-http/guzzle7-adapter`
|
||||||
|
- `php-http/curl-client`
|
||||||
|
- `symfony/http-client`
|
||||||
|
- And other PSR-18 compatible implementations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install any PSR-18 client - discovery works automatically
|
||||||
|
composer require php-http/guzzle7-adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Connecting to Servers
|
||||||
|
|
||||||
|
### Establishing Connection
|
||||||
|
|
||||||
|
```php
|
||||||
|
$client->connect($transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `connect()` method performs the MCP initialization handshake:
|
||||||
|
1. Opens the transport connection
|
||||||
|
2. Sends InitializeRequest with client capabilities
|
||||||
|
3. Waits for InitializeResult from server
|
||||||
|
4. Sends InitializedNotification
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Always wrap connection in try/catch to handle `ConnectionException` for failed connections.
|
||||||
|
|
||||||
|
### Checking Connection State
|
||||||
|
|
||||||
|
```php
|
||||||
|
if ($client->isConnected()) {
|
||||||
|
// Client is connected and initialized
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disconnecting
|
||||||
|
|
||||||
|
```php
|
||||||
|
$client->disconnect();
|
||||||
|
```
|
||||||
|
|
||||||
|
Always disconnect when finished to clean up resources:
|
||||||
|
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
$client->connect($transport);
|
||||||
|
// ... use the client ...
|
||||||
|
} finally {
|
||||||
|
$client->disconnect();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Information
|
||||||
|
|
||||||
|
After successful connection, retrieve server metadata:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Get server implementation info
|
||||||
|
$serverInfo = $client->getServerInfo();
|
||||||
|
echo "Server: {$serverInfo->name} v{$serverInfo->version}\n";
|
||||||
|
|
||||||
|
// Get server instructions
|
||||||
|
$instructions = $client->getInstructions();
|
||||||
|
if ($instructions) {
|
||||||
|
echo "Instructions: {$instructions}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Tools
|
||||||
|
|
||||||
|
### Listing Tools
|
||||||
|
|
||||||
|
```php
|
||||||
|
$toolsResult = $client->listTools();
|
||||||
|
|
||||||
|
foreach ($toolsResult->tools as $tool) {
|
||||||
|
echo "- {$tool->name}: {$tool->description}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pagination
|
||||||
|
if ($toolsResult->nextCursor) {
|
||||||
|
$moreTools = $client->listTools($toolsResult->nextCursor);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Calling Tools
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $client->callTool(
|
||||||
|
name: 'calculate',
|
||||||
|
arguments: ['a' => 5, 'b' => 3, 'operation' => 'add'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access results
|
||||||
|
foreach ($result->content as $content) {
|
||||||
|
if ($content instanceof TextContent) {
|
||||||
|
echo $content->text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progress Notifications
|
||||||
|
|
||||||
|
Hook into tool execution progress (if server supports it):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $client->callTool(
|
||||||
|
name: 'long_running_task',
|
||||||
|
arguments: ['data' => 'large_dataset'],
|
||||||
|
onProgress: static function (float $progress, ?float $total, ?string $message) {
|
||||||
|
$percent = $total > 0 ? round(($progress / $total) * 100) : 0;
|
||||||
|
echo "Progress: {$percent}% - {$message}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Progress notifications are only received if the server sends them. The callback will not be invoked if the server doesn't support or send progress updates.
|
||||||
|
|
||||||
|
## Working with Resources
|
||||||
|
|
||||||
|
### Listing Resources
|
||||||
|
|
||||||
|
```php
|
||||||
|
$resourcesResult = $client->listResources();
|
||||||
|
|
||||||
|
foreach ($resourcesResult->resources as $resource) {
|
||||||
|
echo "- {$resource->uri}: {$resource->name}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listing Resource Templates
|
||||||
|
|
||||||
|
```php
|
||||||
|
$templatesResult = $client->listResourceTemplates();
|
||||||
|
|
||||||
|
foreach ($templatesResult->resourceTemplates as $template) {
|
||||||
|
echo "- {$template->uriTemplate}: {$template->name}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading Resources
|
||||||
|
|
||||||
|
```php
|
||||||
|
$resourceResult = $client->readResource('config://app/settings');
|
||||||
|
|
||||||
|
foreach ($resourceResult->contents as $content) {
|
||||||
|
if ($content instanceof TextResourceContents) {
|
||||||
|
echo "Text: {$content->text}\n";
|
||||||
|
} elseif ($content instanceof BlobResourceContents) {
|
||||||
|
echo "Binary data (base64): {$content->blob}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resources also support progress notifications:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $client->readResource(
|
||||||
|
uri: 'file://large-file.bin',
|
||||||
|
onProgress: static function (float $progress, ?float $total, ?string $message) {
|
||||||
|
echo "Reading: {$progress}/{$total} bytes\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Prompts
|
||||||
|
|
||||||
|
### Listing Prompts
|
||||||
|
|
||||||
|
```php
|
||||||
|
$promptsResult = $client->listPrompts();
|
||||||
|
|
||||||
|
foreach ($promptsResult->prompts as $prompt) {
|
||||||
|
echo "- {$prompt->name}: {$prompt->description}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Prompts
|
||||||
|
|
||||||
|
```php
|
||||||
|
$promptResult = $client->getPrompt(
|
||||||
|
name: 'code_review',
|
||||||
|
arguments: ['language' => 'php', 'code' => '...'],
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($promptResult->messages as $message) {
|
||||||
|
echo "{$message->role->value}: {$message->content->text}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Prompts also support progress notifications:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$result = $client->getPrompt(
|
||||||
|
name: 'generate_report',
|
||||||
|
arguments: ['topic' => 'quarterly_analysis'],
|
||||||
|
onProgress: static function (float $progress, ?float $total, ?string $message) {
|
||||||
|
echo "Generating: {$message}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requesting Completions
|
||||||
|
|
||||||
|
Request auto-completion suggestions for prompt or resource arguments:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Schema\PromptReference;
|
||||||
|
|
||||||
|
$completionResult = $client->complete(
|
||||||
|
ref: new PromptReference('code_review'),
|
||||||
|
argument: ['name' => 'language', 'value' => 'ph'],
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($completionResult->values as $value) {
|
||||||
|
echo "Suggestion: {$value}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server-Initiated Communication
|
||||||
|
|
||||||
|
The client can receive requests and notifications from the server when configured with appropriate handlers.
|
||||||
|
|
||||||
|
### Logging Notifications
|
||||||
|
|
||||||
|
Receive structured log messages from the server:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
|
||||||
|
use Mcp\Schema\Notification\LoggingMessageNotification;
|
||||||
|
use Mcp\Schema\Enum\LoggingLevel;
|
||||||
|
|
||||||
|
$loggingHandler = new LoggingNotificationHandler(
|
||||||
|
static function (LoggingMessageNotification $notification) {
|
||||||
|
// Route to your application's logging system
|
||||||
|
$level = $notification->level;
|
||||||
|
$message = $notification->data;
|
||||||
|
|
||||||
|
match ($level) {
|
||||||
|
LoggingLevel::Debug => logger()->debug($message),
|
||||||
|
LoggingLevel::Info => logger()->info($message),
|
||||||
|
LoggingLevel::Warning => logger()->warning($message),
|
||||||
|
LoggingLevel::Error => logger()->error($message),
|
||||||
|
default => logger()->info($message),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->addNotificationHandler($loggingHandler)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
// Set minimum log level (optional)
|
||||||
|
$client->setLoggingLevel(LoggingLevel::Info);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sampling (LLM Requests)
|
||||||
|
|
||||||
|
Handle server requests for LLM completions:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Handler\Request\SamplingRequestHandler;
|
||||||
|
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
|
||||||
|
use Mcp\Exception\SamplingException;
|
||||||
|
use Mcp\Schema\ClientCapabilities;
|
||||||
|
use Mcp\Schema\Request\CreateSamplingMessageRequest;
|
||||||
|
use Mcp\Schema\Result\CreateSamplingMessageResult;
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
use Mcp\Schema\Enum\Role;
|
||||||
|
|
||||||
|
class LlmSamplingCallback implements SamplingCallbackInterface
|
||||||
|
{
|
||||||
|
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Call your LLM provider
|
||||||
|
$response = $this->llmClient->complete(
|
||||||
|
messages: $request->messages,
|
||||||
|
maxTokens: $request->maxTokens,
|
||||||
|
temperature: $request->temperature ?? 0.7,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new CreateSamplingMessageResult(
|
||||||
|
role: Role::Assistant,
|
||||||
|
content: new TextContent($response->text),
|
||||||
|
model: $response->model,
|
||||||
|
stopReason: $response->stopReason,
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Throw SamplingException to surface error to server
|
||||||
|
throw new SamplingException(
|
||||||
|
"LLM sampling failed: {$e->getMessage()}",
|
||||||
|
(int) $e->getCode(),
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setCapabilities(new ClientCapabilities(sampling: true))
|
||||||
|
->addRequestHandler(new SamplingRequestHandler(new LlmSamplingCallback))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Error Handling in Sampling Callbacks:**
|
||||||
|
>
|
||||||
|
> When implementing sampling callbacks, error handling is critical:
|
||||||
|
>
|
||||||
|
> - **Throw `SamplingException`** to forward specific error messages to the server
|
||||||
|
> - **Any other exception** will be logged but return a generic error to the server
|
||||||
|
>
|
||||||
|
> This distinction allows you to control what error information the server receives:
|
||||||
|
>
|
||||||
|
> ```php
|
||||||
|
> // Good: Server receives "Rate limit exceeded" message
|
||||||
|
> throw new SamplingException('Rate limit exceeded. Retry after 60 seconds.');
|
||||||
|
>
|
||||||
|
> // Bad: Server receives generic "Error while sampling LLM" message
|
||||||
|
> throw new \RuntimeException('Rate limit exceeded');
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### Elicitation (User Input Requests)
|
||||||
|
|
||||||
|
Handle server requests to elicit additional information from the user during tool
|
||||||
|
execution. The server sends an `elicitation/create` request describing the fields it
|
||||||
|
needs; your callback presents them to the user and returns an `ElicitResult` with one of
|
||||||
|
three actions — accept (with the collected content), decline, or cancel:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Client\Handler\Request\ElicitationRequestHandler;
|
||||||
|
use Mcp\Client\Handler\Request\ElicitationCallbackInterface;
|
||||||
|
use Mcp\Exception\ElicitationException;
|
||||||
|
use Mcp\Schema\ClientCapabilities;
|
||||||
|
use Mcp\Schema\Enum\ElicitAction;
|
||||||
|
use Mcp\Schema\Request\ElicitRequest;
|
||||||
|
use Mcp\Schema\Result\ElicitResult;
|
||||||
|
|
||||||
|
class ConsoleElicitationCallback implements ElicitationCallbackInterface
|
||||||
|
{
|
||||||
|
public function __invoke(ElicitRequest $request): ElicitResult
|
||||||
|
{
|
||||||
|
echo $request->message.\PHP_EOL;
|
||||||
|
|
||||||
|
// Present $request->requestedSchema->properties to the user and collect input.
|
||||||
|
$content = [];
|
||||||
|
foreach ($request->requestedSchema->properties as $name => $definition) {
|
||||||
|
$answer = readline($definition->title.': ');
|
||||||
|
|
||||||
|
if (false === $answer) {
|
||||||
|
// No input available — let the server know the user cancelled.
|
||||||
|
return new ElicitResult(ElicitAction::Cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content[$name] = $answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ElicitResult(ElicitAction::Accept, $content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = Client::builder()
|
||||||
|
->setCapabilities(new ClientCapabilities(elicitation: true))
|
||||||
|
->addRequestHandler(new ElicitationRequestHandler(new ConsoleElicitationCallback))
|
||||||
|
->build();
|
||||||
|
```
|
||||||
|
|
||||||
|
Return `new ElicitResult(ElicitAction::Decline)` when the user refuses to provide the
|
||||||
|
information, and `new ElicitResult(ElicitAction::Cancel)` when they dismiss the request.
|
||||||
|
Only the `Accept` action carries content.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Error Handling in Elicitation Callbacks:**
|
||||||
|
>
|
||||||
|
> - **Throw `ElicitationException`** to forward a specific error message to the server
|
||||||
|
> - **Any other exception** is logged but returns a generic error to the server
|
||||||
|
>
|
||||||
|
> ```php
|
||||||
|
> // Good: Server receives "No interactive console available" message
|
||||||
|
> throw new ElicitationException('No interactive console available');
|
||||||
|
>
|
||||||
|
> // Bad: Server receives generic "Error while processing elicitation" message
|
||||||
|
> throw new \RuntimeException('No interactive console available');
|
||||||
|
> ```
|
||||||
|
|
||||||
|
See `examples/client/stdio_elicitation.php` for a runnable example against the
|
||||||
|
elicitation demo server.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The client throws exceptions for various error conditions:
|
||||||
|
|
||||||
|
### ConnectionException
|
||||||
|
|
||||||
|
Thrown when connection or initialization fails:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Exception\ConnectionException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client->connect($transport);
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
echo "Failed to connect: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RequestException
|
||||||
|
|
||||||
|
Thrown when a request returns an error response:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Mcp\Exception\RequestException;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $client->callTool('unknown_tool', []);
|
||||||
|
} catch (RequestException $e) {
|
||||||
|
echo "Request failed: {$e->getMessage()}\n";
|
||||||
|
echo "Error code: {$e->getCode()}\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
Here's a comprehensive example demonstrating client usage:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Mcp\Client;
|
||||||
|
use Mcp\Client\Handler\Notification\LoggingNotificationHandler;
|
||||||
|
use Mcp\Client\Handler\Request\SamplingCallbackInterface;
|
||||||
|
use Mcp\Client\Handler\Request\SamplingRequestHandler;
|
||||||
|
use Mcp\Client\Transport\StdioTransport;
|
||||||
|
use Mcp\Exception\SamplingException;
|
||||||
|
use Mcp\Schema\ClientCapabilities;
|
||||||
|
use Mcp\Schema\Content\TextContent;
|
||||||
|
use Mcp\Schema\Enum\LoggingLevel;
|
||||||
|
use Mcp\Schema\Enum\Role;
|
||||||
|
use Mcp\Schema\Notification\LoggingMessageNotification;
|
||||||
|
use Mcp\Schema\Request\CreateSamplingMessageRequest;
|
||||||
|
use Mcp\Schema\Result\CreateSamplingMessageResult;
|
||||||
|
|
||||||
|
// Configure logging notification handler
|
||||||
|
$loggingHandler = new LoggingNotificationHandler(
|
||||||
|
static function (LoggingMessageNotification $notification) {
|
||||||
|
echo "[LOG {$notification->level->value}] {$notification->data}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Configure sampling callback
|
||||||
|
$samplingCallback = new class implements SamplingCallbackInterface {
|
||||||
|
public function __invoke(CreateSamplingMessageRequest $request): CreateSamplingMessageResult
|
||||||
|
{
|
||||||
|
echo "[SAMPLING] Processing request (max {$request->maxTokens} tokens)\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Integration with your LLM provider
|
||||||
|
$response = "This is a mock LLM response for: " .
|
||||||
|
json_encode($request->messages);
|
||||||
|
|
||||||
|
return new CreateSamplingMessageResult(
|
||||||
|
role: Role::Assistant,
|
||||||
|
content: new TextContent($response),
|
||||||
|
model: 'mock-llm',
|
||||||
|
stopReason: 'end_turn',
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new SamplingException(
|
||||||
|
"Sampling failed: {$e->getMessage()}",
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build client
|
||||||
|
$client = Client::builder()
|
||||||
|
->setClientInfo('Example Client', '1.0.0')
|
||||||
|
->setInitTimeout(30)
|
||||||
|
->setRequestTimeout(120)
|
||||||
|
->setCapabilities(new ClientCapabilities(sampling: true))
|
||||||
|
->addNotificationHandler($loggingHandler)
|
||||||
|
->addRequestHandler(new SamplingRequestHandler($samplingCallback))
|
||||||
|
->build();
|
||||||
|
|
||||||
|
// Create transport
|
||||||
|
$transport = new StdioTransport(
|
||||||
|
command: 'php',
|
||||||
|
args: [__DIR__ . '/server.php'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connect and use server
|
||||||
|
try {
|
||||||
|
echo "Connecting to server...\n";
|
||||||
|
$client->connect($transport);
|
||||||
|
|
||||||
|
// Get server info
|
||||||
|
$serverInfo = $client->getServerInfo();
|
||||||
|
echo "Connected to: {$serverInfo->name} v{$serverInfo->version}\n\n";
|
||||||
|
|
||||||
|
// List capabilities
|
||||||
|
echo "Available tools:\n";
|
||||||
|
$tools = $client->listTools();
|
||||||
|
foreach ($tools->tools as $tool) {
|
||||||
|
echo " - {$tool->name}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nAvailable resources:\n";
|
||||||
|
$resources = $client->listResources();
|
||||||
|
foreach ($resources->resources as $resource) {
|
||||||
|
echo " - {$resource->uri}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logging level
|
||||||
|
$client->setLoggingLevel(LoggingLevel::Debug);
|
||||||
|
|
||||||
|
// Call tool with progress
|
||||||
|
echo "\nCalling tool with progress...\n";
|
||||||
|
$result = $client->callTool(
|
||||||
|
name: 'process_data',
|
||||||
|
arguments: ['dataset' => 'large_file.csv'],
|
||||||
|
onProgress: static function (float $progress, ?float $total, ?string $message) {
|
||||||
|
$percent = $total > 0 ? round(($progress / $total) * 100) : 0;
|
||||||
|
echo " Progress: {$percent}% - {$message}\n";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "\nResult:\n";
|
||||||
|
foreach ($result->content as $content) {
|
||||||
|
if ($content instanceof TextContent) {
|
||||||
|
echo $content->text . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "Error: {$e->getMessage()}\n";
|
||||||
|
echo $e->getTraceAsString() . "\n";
|
||||||
|
} finally {
|
||||||
|
$client->disconnect();
|
||||||
|
echo "\nDisconnected.\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue