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
2 changes: 1 addition & 1 deletion .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- symfony-version: '7.4.*'
dependencies: 'highest'
php: '8.4'
- symfony-version: '8.0.*'
- symfony-version: '8.1.*'
dependencies: 'highest'
php: '8.4'

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ All notable changes to `mcp/sdk` will be documented in this file.

* Fix variadic tool parameters (e.g. `int ...$scores`) failing with a cast error: each element of the array a client sends is now cast on its own.
* Fix `Builder::build()` with a registry passed to `setRegistry()` publishing one `list_changed` notification per loaded element. Adds `Registry::loadFrom()`.
* 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": {
"firebase/php-jwt": "Required for the JwtTokenValidator.",
Expand Down
102 changes: 102 additions & 0 deletions docs/advanced/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,106 @@ 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. The
> name follows the Agent Skills rules: 1 to 64 characters of lowercase letters,
> digits and single hyphens, with no hyphen at the start or end. The description
> has at most 1024 characters. A violation throws an `InvalidArgumentException`.

Hidden files and folders (`.env`, `.git/`) are neither listed nor served. File
and folder names are percent-encoded in the URIs, so `my docs/a b.md` becomes
`skill://my-skill/my%20docs/a%20b.md`.

The server declares the `resources` capability whenever the extension is enabled.
`setCapabilities()` therefore has to keep `resources` on.

The frontmatter is sent as parsed, so a host that compares it with the file sees
the same content. One limit: YAML turns an unquoted date like `2025-01-02` into a
timestamp. Quote such values (`"2025-01-02"`) to keep them as strings.

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) | Replacing the built-in `tools/list` and `tools/call` request handlers | [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];
}
}
Loading
Loading