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
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.0 || ^2.0 || ^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 Expand Up @@ -80,6 +81,7 @@
"Mcp\\Example\\Server\\OAuthKeycloak\\": "examples/server/oauth-keycloak/",
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
"Mcp\\Example\\Server\\Skills\\": "examples/server/skills/",
"Mcp\\Tests\\": "tests/"
},
"classmap": [
Expand Down
12 changes: 12 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,18 @@ and calls back into the server. See the
[ext-apps repo](https://github.com/modelcontextprotocol/ext-apps) for the
TypeScript SDK and richer view-side patterns.

### Skills

**File**: `examples/server/skills/`

A directory of skills exposed through the [Skills extension](extensions.md)
(SEP-2640). `addSkillsFromDirectory()` registers each `SKILL.md` (and its
supporting files) as a `skill://` resource, derives `name`/`description` from the
YAML frontmatter, and serves a `skill://index.json` discovery index. Demonstrates
flat (`skill://code-review/SKILL.md`), nested
(`skill://acme/billing/refunds/SKILL.md`), and supporting-file
(`skill://code-review/references/SECURITY.md`) URIs.

## Client Examples

### STDIO Discovery Calculator (Client)
Expand Down
79 changes: 79 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,83 @@ TypeScript SDK (`@modelcontextprotocol/ext-apps`), and view-side examples. A
working minimal view is included in
[`examples/server/mcp-apps/weather-app.html`](../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. Skills are served through the existing **Resources** primitive with
zero protocol changes: each skill is a `skill://<skill-path>/SKILL.md` resource
(plus any supporting files), and the server advertises an empty
`io.modelcontextprotocol/skills` capability.

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:

```
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 supplies the
resource `name`/`description`; any remaining frontmatter keys are exposed under the
`io.modelcontextprotocol.skills/` `_meta` namespace. 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`); a mismatch throws an
> `InvalidArgumentException`.

By default a discovery index is also served at `skill://index.json` (an
[Agent Skills][agent-skills] discovery document listing every skill). Skills also
appear as normal entries in `resources/list`, so a large skill tree pages via
`resources/list` cursors. Pass `withDiscoveryIndex: false` to skip the index.

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 marker; provides `EXTENSION_ID`, `MIME_TYPE`, `URI_SCHEME`, `ENTRY_POINT`, `DISCOVERY_URI`, `META_PREFIX` constants. |
| `SkillProvider` | Walks a directory and registers each skill (and its files) as `skill://` resources. |
| `FrontmatterParser` | Splits a `SKILL.md` into its YAML frontmatter and markdown body. |
| `SkillMetadata` | Value object for parsed frontmatter: `name`, `description`, `extra`. |
| `SkillDiscoveryIndex` | The `skill://index.json` document: `$schema` + `skills`. |
| `SkillDiscoveryEntry` | One index entry: `name`, `type`, `url`, `description`. |
| `SkillType` | Enum: `SkillMd` (`skill-md`), `McpResourceTemplate` (`mcp-resource-template`). |

A complete example lives in
[`examples/server/skills/`](../examples/server/skills/).

[ext-apps]: https://github.com/modelcontextprotocol/ext-apps
[ext-skills]: https://github.com/modelcontextprotocol/experimental-ext-skills
[agent-skills]: https://agentskills.io
[symfony-yaml]: https://symfony.com/doc/current/components/yaml.html
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 through the existing MCP **Resources**
primitive, with zero protocol changes.

## 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.

## 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
```

Plus a discovery index at `skill://index.json` listing every skill.

## 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; any extra frontmatter is exposed
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.
59 changes: 59 additions & 0 deletions src/Schema/Extension/Skills/McpSkills.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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\Schema\Extension\ServerExtensionInterface;

/**
* The MCP Skills extension (io.modelcontextprotocol/skills).
*
* Skills are multi-step workflow instructions ("how to orchestrate tools") that a server ships
* alongside its tools. Per SEP-2640 they are served through the existing Resources primitive with
* zero protocol changes: each skill is exposed as a `skill://<skill-path>/SKILL.md` resource (plus
* any supporting files), and the server advertises this extension during capability negotiation.
*
* Enable on the server via {@see \Mcp\Server\Builder::enableExtension()}, or use the
* {@see \Mcp\Server\Builder::addSkillsFromDirectory()} convenience to expose a directory of skills.
*
* @see https://github.com/modelcontextprotocol/experimental-ext-skills
*
* @author Johannes Wachter <johannes@sulu.io>
*/
final class McpSkills implements ServerExtensionInterface
{
public const EXTENSION_ID = 'io.modelcontextprotocol/skills';
public const MIME_TYPE = 'text/markdown';
public const URI_SCHEME = 'skill';
public const ENTRY_POINT = 'SKILL.md';
public const DISCOVERY_URI = 'skill://index.json';

/**
* The (not-yet-standardized) `_meta` namespace prefix under which extra SKILL.md frontmatter
* fields are exposed on a skill resource descriptor.
*/
public const META_PREFIX = 'io.modelcontextprotocol.skills/';

public function getId(): string
{
return self::EXTENSION_ID;
}

/**
* The Skills extension advertises an empty capability payload (`{}`).
*
* @return array<string, mixed>
*/
public function getCapabilities(): array
{
return [];
}
}
Loading