Skip to content

Commit 4f2d3fc

Browse files
committed
Add Metadata injection support for tool handlers
- Introduced `Mcp\Schema\Metadata` value object to handle request-scoped metadata. - Updated `ReferenceHandler` to inject `Metadata` into handler parameters based on type hints. - Enhanced `CallToolHandler` to pass `_meta` from requests to handler arguments. - Added unit tests to verify `Metadata` injection behavior and error handling. - Updated documentation to include examples and guidelines for `Metadata` usage.
1 parent 470237a commit 4f2d3fc

File tree

5 files changed

+206
-0
lines changed

5 files changed

+206
-0
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,63 @@ $server = Server::builder()
248248
->build();
249249
```
250250

251+
### Request Metadata Injection
252+
253+
You can access request-scoped metadata inside tool handlers by type-hinting the `Mcp\Schema\Metadata` value object. The SDK will inject it automatically when present on the request.
254+
255+
How it works:
256+
- Clients may send arbitrary metadata in `params._meta` of the JSON-RPC request.
257+
- The server forwards this metadata for tool calls and the `ReferenceHandler` injects it into parameters typed as `Metadata` or `?Metadata`.
258+
- If your handler declares a non-nullable `Metadata` parameter but the request contains no `params._meta`, the SDK returns an internal error. Use `?Metadata` if it is optional.
259+
260+
Example handler usage:
261+
262+
```php
263+
<?php
264+
265+
use Mcp\Capability\Attribute\McpTool;
266+
use Mcp\Schema\Metadata;
267+
268+
final class ExampleTools
269+
{
270+
#[McpTool(name: 'example_action')]
271+
public function exampleAction(string $input, Metadata $meta): array
272+
{
273+
$schema = $meta->get('securitySchema');
274+
275+
return [
276+
'result' => 'ok',
277+
'securitySchema' => $schema,
278+
];
279+
}
280+
281+
#[McpTool(name: 'example_optional')]
282+
public function exampleOptional(string $input, ?Metadata $meta = null): string
283+
{
284+
return $meta?->get('traceId') ?? 'no-meta';
285+
}
286+
}
287+
```
288+
289+
Calling a tool with metadata (JSON-RPC example):
290+
291+
```json
292+
{
293+
"jsonrpc": "2.0",
294+
"id": "1",
295+
"method": "tools/call",
296+
"params": {
297+
"name": "example_action",
298+
"arguments": { "input": "hello" },
299+
"_meta": { "securitySchema": "secure-123", "traceId": "abc-xyz" }
300+
}
301+
}
302+
```
303+
304+
Notes:
305+
- For non-nullable `Metadata` parameters, the client must provide `params._meta`; otherwise an internal error is returned.
306+
- For nullable `?Metadata` parameters, `null` will be injected when `params._meta` is absent.
307+
251308
## Documentation
252309

253310
**Core Concepts:**

src/Capability/Registry/ReferenceHandler.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Mcp\Server\ClientAwareInterface;
1717
use Mcp\Server\ClientGateway;
1818
use Mcp\Server\Session\SessionInterface;
19+
use Mcp\Schema\Metadata;
1920
use Psr\Container\ContainerInterface;
2021

2122
/**
@@ -112,6 +113,21 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
112113
$finalArgs[$paramPosition] = new ClientGateway($arguments['_session']);
113114
continue;
114115
}
116+
117+
// Inject request metadata if requested
118+
if (Metadata::class === $typeName) {
119+
if (isset($arguments['_meta']) && \is_array($arguments['_meta'])) {
120+
$finalArgs[$paramPosition] = new Metadata($arguments['_meta']);
121+
} elseif ($parameter->allowsNull()) {
122+
$finalArgs[$paramPosition] = null;
123+
} else {
124+
$reflectionName = $reflection instanceof \ReflectionMethod
125+
? $reflection->class.'::'.$reflection->name
126+
: 'Closure';
127+
throw RegistryException::internalError("Missing required request metadata for parameter `{$paramName}` in {$reflectionName}. Provide `_meta` in request params or make the parameter nullable.");
128+
}
129+
continue;
130+
}
115131
}
116132

117133
if (isset($arguments[$paramName])) {

src/Schema/Metadata.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Schema;
13+
14+
/**
15+
* Lightweight value object to access request metadata in handlers.
16+
*
17+
* Example usage in a tool handler:
18+
* function exampleAction(string $input, Metadata $meta): array {
19+
* $schema = $meta->get('securitySchema');
20+
* return ['result' => 'ok', 'securitySchema' => $schema];
21+
* }
22+
*
23+
* The SDK will inject an instance automatically when the parameter is type-hinted
24+
* with this class. If no metadata is present on the request and the parameter
25+
* allows null, null will be passed; otherwise, an internal error will be thrown.
26+
*/
27+
final class Metadata
28+
{
29+
/**
30+
* @param array<string, mixed> $data
31+
*/
32+
public function __construct(private array $data = [])
33+
{
34+
}
35+
36+
/**
37+
* @return array<string, mixed>
38+
*/
39+
public function all(): array
40+
{
41+
return $this->data;
42+
}
43+
44+
public function has(string $key): bool
45+
{
46+
return \array_key_exists($key, $this->data);
47+
}
48+
49+
public function get(string $key, mixed $default = null): mixed
50+
{
51+
return $this->data[$key] ?? $default;
52+
}
53+
}

src/Server/Handler/Request/CallToolHandler.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public function handle(Request $request, SessionInterface $session): Response|Er
6161
$reference = $this->registry->getTool($toolName);
6262

6363
$arguments['_session'] = $session;
64+
// Pass request metadata through to the handler so it can be injected
65+
// into method parameters when requested by type-hint.
66+
if (null !== $request->getMeta()) {
67+
$arguments['_meta'] = $request->getMeta();
68+
}
6469

6570
$result = $this->referenceHandler->handle($reference, $arguments);
6671

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*/
8+
9+
namespace Mcp\Tests\Unit\Capability\Registry;
10+
11+
use Mcp\Capability\Registry\ElementReference;
12+
use Mcp\Capability\Registry\ReferenceHandler;
13+
use Mcp\Schema\Metadata;
14+
use Mcp\Server\Session\SessionInterface;
15+
use PHPUnit\Framework\MockObject\MockObject;
16+
use PHPUnit\Framework\TestCase;
17+
18+
final class ReferenceHandlerTest extends TestCase
19+
{
20+
private ReferenceHandler $handler;
21+
private SessionInterface&MockObject $session;
22+
23+
protected function setUp(): void
24+
{
25+
$this->handler = new ReferenceHandler();
26+
$this->session = $this->createMock(SessionInterface::class);
27+
}
28+
29+
public function testInjectsMetadataIntoTypedParameter(): void
30+
{
31+
$fn = function (Metadata $meta): string {
32+
return (string) $meta->get('securitySchema');
33+
};
34+
35+
$ref = new ElementReference($fn);
36+
37+
$result = $this->handler->handle($ref, [
38+
'_session' => $this->session,
39+
'_meta' => ['securitySchema' => 'secure-123'],
40+
]);
41+
42+
$this->assertSame('secure-123', $result);
43+
}
44+
45+
public function testNullableMetadataReceivesNullWhenNotProvided(): void
46+
{
47+
$fn = function (?Metadata $meta): string {
48+
return null === $meta ? 'no-meta' : 'has-meta';
49+
};
50+
51+
$ref = new ElementReference($fn);
52+
53+
$result = $this->handler->handle($ref, [
54+
'_session' => $this->session,
55+
]);
56+
57+
$this->assertSame('no-meta', $result);
58+
}
59+
60+
public function testRequiredMetadataThrowsInternalErrorWhenNotProvided(): void
61+
{
62+
$fn = function (Metadata $meta): array {
63+
return $meta->all();
64+
};
65+
66+
$ref = new ElementReference($fn);
67+
68+
$this->expectException(\Mcp\Exception\RegistryException::class);
69+
$this->expectExceptionMessage('Missing required request metadata');
70+
71+
$this->handler->handle($ref, [
72+
'_session' => $this->session,
73+
]);
74+
}
75+
}

0 commit comments

Comments
 (0)