Skip to content
Open
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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,63 @@ $server = Server::builder()
->build();
```

### Request Metadata Injection

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.

How it works:
- Clients may send arbitrary metadata in `params._meta` of the JSON-RPC request.
- The server forwards this metadata for tool calls and the `ReferenceHandler` injects it into parameters typed as `Metadata` or `?Metadata`.
- 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.

Example handler usage:

```php
<?php

use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\Metadata;

final class ExampleTools
{
#[McpTool(name: 'example_action')]
public function exampleAction(string $input, Metadata $meta): array
{
$schema = $meta->get('securitySchema');

return [
'result' => 'ok',
'securitySchema' => $schema,
];
}

#[McpTool(name: 'example_optional')]
public function exampleOptional(string $input, ?Metadata $meta = null): string
{
return $meta?->get('traceId') ?? 'no-meta';
}
}
```

Calling a tool with metadata (JSON-RPC example):

```json
{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "example_action",
"arguments": { "input": "hello" },
"_meta": { "securitySchema": "secure-123", "traceId": "abc-xyz" }
}
}
```

Notes:
- For non-nullable `Metadata` parameters, the client must provide `params._meta`; otherwise an internal error is returned.
- For nullable `?Metadata` parameters, `null` will be injected when `params._meta` is absent.

## Documentation

**Core Concepts:**
Expand Down
16 changes: 16 additions & 0 deletions src/Capability/Registry/ReferenceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Mcp\Exception\InvalidArgumentException;
use Mcp\Exception\RegistryException;
use Mcp\Schema\Metadata;
use Mcp\Server\ClientAwareInterface;
use Mcp\Server\ClientGateway;
use Mcp\Server\Session\SessionInterface;
Expand Down Expand Up @@ -112,6 +113,21 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array
$finalArgs[$paramPosition] = new ClientGateway($arguments['_session']);
continue;
}

// Inject request metadata if requested
if (Metadata::class === $typeName) {
if (isset($arguments['_meta']) && \is_array($arguments['_meta'])) {
$finalArgs[$paramPosition] = new Metadata($arguments['_meta']);
} elseif ($parameter->allowsNull()) {
$finalArgs[$paramPosition] = null;
} else {
$reflectionName = $reflection instanceof \ReflectionMethod
? $reflection->class.'::'.$reflection->name
: 'Closure';
throw RegistryException::internalError("Missing required request metadata for parameter `{$paramName}` in {$reflectionName}. Provide `_meta` in request params or make the parameter nullable.");
}
continue;
}
}

if (isset($arguments[$paramName])) {
Expand Down
53 changes: 53 additions & 0 deletions src/Schema/Metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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;

/**
* Lightweight value object to access request metadata in handlers.
*
* Example usage in a tool handler:
* function exampleAction(string $input, Metadata $meta): array {
* $schema = $meta->get('securitySchema');
* return ['result' => 'ok', 'securitySchema' => $schema];
* }
*
* The SDK will inject an instance automatically when the parameter is type-hinted
* with this class. If no metadata is present on the request and the parameter
* allows null, null will be passed; otherwise, an internal error will be thrown.
*/
final class Metadata
{
/**
* @param array<string, mixed> $data
*/
public function __construct(private array $data = [])
{
}

/**
* @return array<string, mixed>
*/
public function all(): array
{
return $this->data;
}

public function has(string $key): bool
{
return \array_key_exists($key, $this->data);
}

public function get(string $key, mixed $default = null): mixed
{
return $this->data[$key] ?? $default;
}
}
5 changes: 5 additions & 0 deletions src/Server/Handler/Request/CallToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public function handle(Request $request, SessionInterface $session): Response|Er
$reference = $this->registry->getTool($toolName);

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

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

Expand Down
78 changes: 78 additions & 0 deletions tests/Unit/Capability/Registry/ReferenceHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\Tests\Unit\Capability\Registry;

use Mcp\Capability\Registry\ElementReference;
use Mcp\Capability\Registry\ReferenceHandler;
use Mcp\Schema\Metadata;
use Mcp\Server\Session\SessionInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

final class ReferenceHandlerTest extends TestCase
{
private ReferenceHandler $handler;
private SessionInterface&MockObject $session;

protected function setUp(): void
{
$this->handler = new ReferenceHandler();
$this->session = $this->createMock(SessionInterface::class);
}

public function testInjectsMetadataIntoTypedParameter(): void
{
$fn = function (Metadata $meta): string {
return (string) $meta->get('securitySchema');
};

$ref = new ElementReference($fn);

$result = $this->handler->handle($ref, [
'_session' => $this->session,
'_meta' => ['securitySchema' => 'secure-123'],
]);

$this->assertSame('secure-123', $result);
}

public function testNullableMetadataReceivesNullWhenNotProvided(): void
{
$fn = function (?Metadata $meta): string {
return null === $meta ? 'no-meta' : 'has-meta';
};

$ref = new ElementReference($fn);

$result = $this->handler->handle($ref, [
'_session' => $this->session,
]);

$this->assertSame('no-meta', $result);
}

public function testRequiredMetadataThrowsInternalErrorWhenNotProvided(): void
{
$fn = function (Metadata $meta): array {
return $meta->all();
};

$ref = new ElementReference($fn);

$this->expectException(\Mcp\Exception\RegistryException::class);
$this->expectExceptionMessage('Missing required request metadata');

$this->handler->handle($ref, [
'_session' => $this->session,
]);
}
}
Loading