Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
* [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).
* Add the Skills extension (`io.modelcontextprotocol/skills`, SEP-2640). `Builder::addSkillsFromDirectory()` serves a directory of `SKILL.md` skills as `skill://` resources, and `skills/list`/`skills/get` answer with a complete, digest-and-size manifest per skill.

0.8.0
-----
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"psr/http-server-middleware": "^1.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",
"symfony/yaml": "^5.4 || ^6.4 || ^7.3 || ^8.0"
},
"suggest": {
"symfony/finder": "Required for file-based discovery."
Expand Down
89 changes: 89 additions & 0 deletions docs/advanced/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,93 @@ TypeScript SDK (`@modelcontextprotocol/ext-apps`), and view-side examples. A
working minimal view is included in
[`examples/server/mcp-apps/weather-app.html`](https://github.com/modelcontextprotocol/php-sdk/blob/main/examples/server/mcp-apps/weather-app.html).

## Skills (`io.modelcontextprotocol/skills`)

The [Skills extension][ext-skills] (SEP-2640) lets servers ship **skills** —
multi-step workflow instructions that tell an agent *how to orchestrate* tools to
reach a goal. Each skill file is served through the existing **Resources**
primitive (`skill://<skill-path>/SKILL.md` plus any supporting files), and the
extension adds two mandatory RPC methods:

- `skills/list` — enumerates the skills a server serves, paginated like
`resources/list`.
- `skills/get` — returns the entry for a single skill by its `SKILL.md` URI.

Both return a `Skill` entry: the skill's frontmatter verbatim, and a complete,
`{uri, digest, size}` manifest of every file the skill comprises (`SKILL.md`
included), so a host can build its registry, present the skill for approval, and
verify every later read without fetching anything first.

The simplest way to expose a directory of skills is `addSkillsFromDirectory()`,
which auto-enables the extension and registers every skill it finds:

```php
use Mcp\Server;

$server = Server::builder()
->setServerInfo('My Server', '1.0.0')
->addSkillsFromDirectory(__DIR__.'/skills')
->build();
```

Given this layout, the following `skill://` resources are registered, and a
matching `Skill` entry is added to the `skills/list`/`skills/get` catalog:

```
skills/
├── code-review/
│ ├── SKILL.md → skill://code-review/SKILL.md
│ └── references/SECURITY.md → skill://code-review/references/SECURITY.md
└── acme/billing/refunds/
└── SKILL.md → skill://acme/billing/refunds/SKILL.md
```

Each `SKILL.md` is served as `text/markdown`. Its YAML frontmatter's `name` and
`description` become the resource `name`/`description`; any remaining frontmatter
keys are exposed under the `io.modelcontextprotocol.skills/` `_meta` namespace on
the resource, and pass through verbatim in the `skills/list`/`skills/get` entry's
`frontmatter`. Supporting files are served with a MIME type guessed from their
extension/content.

```yaml
---
name: code-review
description: Review a pull request for correctness, security, and style.
version: 1.0.0
tags: [review, quality]
---

# Code Review
...
```

> The frontmatter `name` **must** equal the final segment of the skill's directory
> path (`code-review/` → `name: code-review`), and `description` is required; a
> violation throws an `InvalidArgumentException`.

The extension fixes two per-skill limits so every conforming host knows what it
must accept: 512 resources and 16 MiB total content. `addSkillsFromDirectory()`
throws if a skill exceeds either.

Parsing `SKILL.md` frontmatter requires the [`symfony/yaml`][symfony-yaml]
component, which is a dependency of this SDK.

### Server-side classes

| Class | Purpose |
| --- | --- |
| `McpSkills` | Extension; provides `EXTENSION_ID`, `MIME_TYPE`, `URI_SCHEME`, `ENTRY_POINT`, `META_PREFIX` constants and the `skills/list`/`skills/get` handlers. |
| `SkillProvider` | Walks a directory and registers each skill (and its files) as `skill://` resources, recording each skill's manifest in a `SkillRegistry`. |
| `SkillRegistry` | The skills a server serves, keyed by `SKILL.md` URI; backs `skills/list`/`skills/get`. |
| `FrontmatterParser` | Splits a `SKILL.md` into its YAML frontmatter and markdown body. |
| `SkillMetadata` | Value object for parsed frontmatter: `name`, `description`, `extra`. |
| `Skill` | One `skills/list`/`skills/get` entry: `uri`, `frontmatter`, `resources`. |
| `SkillResource` | One file of a skill's manifest: `uri`, `digest`, `size`. |

A complete example lives in
[`examples/server/skills/`](https://github.com/modelcontextprotocol/php-sdk/blob/main/examples/server/skills/).

[ext-skills]: https://github.com/modelcontextprotocol/ext-skills
[symfony-yaml]: https://github.com/symfony/yaml

[ext-apps]: https://github.com/modelcontextprotocol/ext-apps
1 change: 1 addition & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ npx @modelcontextprotocol/inspector php examples/server/discovery-calculator/ser
| [`elicitation`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/elicitation) | Asking the user for input mid-call with `ClientGateway::elicit()` and typed elicitation schemas, on either protocol era | [Asking for input](handlers/input-required.md) |
| [`custom-method-handlers`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/custom-method-handlers) | Registering handlers for custom JSON-RPC methods | [Custom message handlers](advanced/custom-handlers.md) |
| [`mcp-apps`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/mcp-apps) | The MCP Apps extension: a tool that ships an interactive HTML view | [Protocol extensions](advanced/extensions.md) |
| [`skills`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/skills) | The Skills extension: `skills/list`/`skills/get` over a directory of `SKILL.md` files | [Protocol extensions](advanced/extensions.md) |
| [`stateless-lifecycle`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/stateless-lifecycle) | Revision `2026-07-28`: cache policy, request state, notification bus | [Serving both eras](run/protocol-eras.md), [Caching](run/caching.md), [Subscriptions](run/subscriptions.md) |
| [`oauth-keycloak`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/oauth-keycloak) | OAuth authorization against a Keycloak instance (own README) | [Authorization](run/authorization.md) |
| [`oauth-microsoft`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/oauth-microsoft) | OAuth authorization against Microsoft Entra ID (own README) | [Authorization](run/authorization.md) |
Expand Down
45 changes: 45 additions & 0 deletions examples/server/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# MCP Skills Example

Demonstrates the **Skills extension** (`io.modelcontextprotocol/skills`, SEP-2640): serving
multi-step workflow instructions ("skills") to clients. Each skill file is served through the
existing MCP **Resources** primitive, and the extension adds two RPC methods, `skills/list` and
`skills/get`, that return a complete, digest-and-size manifest of a skill's files.

## Running

```bash
php examples/server/skills/server.php
```

A single call exposes the whole `skills/` directory:

```php
Server::builder()
->setServerInfo('MCP Skills Example', '1.0.0')
->addSkillsFromDirectory(__DIR__.'/skills')
->build();
```

This auto-enables the `McpSkills` extension and registers every `SKILL.md` (plus supporting
files) as a `skill://` resource, and its manifest as a `skills/list`/`skills/get` entry.

## Layout & URIs

```
skills/
├── code-review/
│ ├── SKILL.md → skill://code-review/SKILL.md
│ └── references/SECURITY.md → skill://code-review/references/SECURITY.md
└── acme/billing/refunds/
└── SKILL.md → skill://acme/billing/refunds/SKILL.md
```

## Conventions

- A skill is any folder containing a `SKILL.md`. Its frontmatter `name` **must** equal the final
segment of the folder path (e.g. `code-review` → `name: code-review`).
- `name`/`description` come from the SKILL.md YAML frontmatter and are always present in the
`skills/list`/`skills/get` entry; any extra frontmatter passes through verbatim, and is also
exposed on the SKILL.md resource under the `io.modelcontextprotocol.skills/` `_meta` namespace.
- Supporting files are served with a MIME type guessed from their extension/content.
- Skills are plain files — no PHP handler class is required.
30 changes: 30 additions & 0 deletions examples/server/skills/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env php
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Server;

logger()->info('Starting MCP Skills Example Server...');

$server = Server::builder()
->setServerInfo('MCP Skills Example', '1.0.0')
->setLogger(logger())
->addSkillsFromDirectory(__DIR__.'/skills')
->build();

$result = $server->run(transport());

logger()->info('Server stopped gracefully.', ['result' => $result]);

shutdown($result);
25 changes: 25 additions & 0 deletions examples/server/skills/skills/acme/billing/refunds/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: refunds
description: Process a customer refund following Acme's billing policy and approval thresholds.
version: 1.0.0
tags:
- billing
- support
---

# Processing Refunds

A nested skill demonstrating multi-segment skill paths (`skill://acme/billing/refunds/SKILL.md`).

## Policy

1. Verify the charge exists and has not already been refunded.
2. Refunds up to $100 may be issued directly.
3. Refunds above $100 require a team lead's approval before issuing.

## Steps

1. Look up the original charge by order ID.
2. Confirm the refund amount does not exceed the charged amount.
3. Issue the refund and record the reason code.
4. Notify the customer with the expected settlement window.
38 changes: 38 additions & 0 deletions examples/server/skills/skills/code-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: code-review
description: Review a pull request for correctness, security, and style following this team's conventions.
version: 1.0.0
tags:
- review
- quality
---

# Code Review

Follow these steps to review a pull request thoroughly and consistently.

## 1. Understand the change

- Read the PR description and linked issue to understand the intended behavior.
- Skim the diff top to bottom before commenting to build a mental model.

## 2. Correctness

- Check edge cases: empty input, nulls, boundary values, concurrency.
- Verify error handling fails fast and preserves context.
- Confirm tests cover the new behavior and actually assert on it.

## 3. Security

- See `references/SECURITY.md` for the security checklist that MUST be applied to
every change touching authentication, input parsing, or external I/O.

## 4. Style & maintainability

- Match the surrounding code's naming, structure, and comment density.
- Prefer the simplest implementation that satisfies the requirement.

## 5. Wrap up

- Summarize findings grouped by severity (blocking, suggestion, nit).
- Approve only when blocking issues are resolved and CI is green.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Security Review Checklist

Apply this checklist to every change that touches authentication, input parsing, or external I/O.

- **Input validation**: All external input is validated and normalized before use.
- **Injection**: Queries, shell commands, and templates use parameterization — never string concatenation.
- **AuthZ**: Every privileged action re-checks the caller's authorization server-side.
- **Secrets**: No credentials, tokens, or keys are logged or committed.
- **Output encoding**: Data rendered into HTML, URLs, or headers is contextually encoded.
- **Dependencies**: New dependencies are pinned and free of known advisories.
54 changes: 54 additions & 0 deletions src/Schema/Extension/Skills/GetSkillRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Schema\Extension\Skills;

use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\JsonRpc\Request;

/**
* Sent from the client to the server, to get the entry for a single skill by
* the URI of its SKILL.md.
*
* @author Johannes Wachter <johannes@sulu.io>
*/
final class GetSkillRequest extends Request
{
/**
* @param non-empty-string $uri URI of the skill's SKILL.md
*/
public function __construct(
public readonly string $uri,
) {
}

public static function getMethod(): string
{
return 'skills/get';
}

protected static function fromParams(?array $params): static
{
if (!isset($params['uri']) || !\is_string($params['uri']) || '' === $params['uri']) {
throw new InvalidArgumentException('Missing or invalid "uri" parameter for skills/get.');
}

return new self($params['uri']);
}

/**
* @return array{uri: non-empty-string}
*/
protected function getParams(): array
{
return ['uri' => $this->uri];
}
}
61 changes: 61 additions & 0 deletions src/Schema/Extension/Skills/GetSkillResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Schema\Extension\Skills;

use Mcp\Exception\InvalidArgumentException;
use Mcp\Schema\Enum\CacheScope;
use Mcp\Schema\JsonRpc\ResultInterface;

/**
* The server's response to a skills/get request from the client.
*
* @author Johannes Wachter <johannes@sulu.io>
*/
final class GetSkillResult implements ResultInterface
{
/**
* @param ?int $ttlMs how long a client may consider this fresh, in milliseconds. Null
* leaves it to the server's configured {@see \Mcp\Server\Wire\CachePolicy}.
* @param ?CacheScope $cacheScope who may cache it. Null defers to the policy.
*/
public function __construct(
public readonly Skill $skill,
public readonly ?int $ttlMs = null,
public readonly ?CacheScope $cacheScope = null,
) {
if (null !== $this->ttlMs && $this->ttlMs < 0) {
throw new InvalidArgumentException(\sprintf('A skills/get "ttlMs" must be zero or more, got %d.', $this->ttlMs));
}
}

/**
* @return array{
* skill: Skill,
* ttlMs?: int,
* cacheScope?: string,
* }
*/
public function jsonSerialize(): array
{
$data = ['skill' => $this->skill];

if (null !== $this->ttlMs) {
$data['ttlMs'] = $this->ttlMs;
}

if (null !== $this->cacheScope) {
$data['cacheScope'] = $this->cacheScope->value;
}

return $data;
}
}
Loading