[Server] Keep injectable parameters out of the published inputSchema (#482)

This commit is contained in:
Christopher Hertel 2026-08-25 01:12:29 +02:00 • committed by GitHub
commit e173367c5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 18 deletions

View file

@ -16,6 +16,7 @@
use Mcp\Exception\ClientException;
use Mcp\Schema\Content\TextContent;
use Mcp\Server;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext;
use Mcp\Server\Transport\StdioTransport;
@ -38,5 +39,20 @@ Server::builder()
name: 'summarize',
description: 'Summarizes text by asking the client to sample.',
)
->addTool(
static function (ClientGateway $client, string $text): string {
try {
$result = $client->sample($text, maxTokens: 64);
} catch (ClientException $e) {
return $e->getMessage();
}
assert($result->content instanceof TextContent);
return sprintf('%s said: %s', $result->model, $result->content->text);
},
name: 'summarize_via_gateway',
description: 'Summarizes text through a directly injected gateway.',
)
->build()
->run(new StdioTransport());

View file

@ -56,6 +56,29 @@ final class SamplingTest extends IntegrationTestCase
$this->assertSame(64, $seen[0]->maxTokens);
}
#[TestDox('a gateway parameter is injected, not published in the schema')]
public function testGatewayParameterIsInjectedNotPublished(): void
{
$client = $this->connect('sampling', $this->clientSampling());
$tool = null;
foreach ($client->listTools()->tools as $candidate) {
if ('summarize_via_gateway' === $candidate->name) {
$tool = $candidate;
}
}
$this->assertNotNull($tool);
$this->assertArrayNotHasKey('client', $tool->inputSchema['properties']);
$this->assertArrayHasKey('text', $tool->inputSchema['properties']);
$this->assertSame(['text'], $tool->inputSchema['required']);
$result = $client->callTool('summarize_via_gateway', ['text' => 'a long report']);
$this->assertInstanceOf(TextContent::class, $result->content[0]);
$this->assertSame('test-model said: a long report', $result->content[0]->text);
}
#[TestDox('a client that cannot sample refuses instead of stalling the tool')]
public function testClientWithoutSamplingRefuses(): void
{

View file

@ -13,6 +13,8 @@ namespace Mcp\Tests\Unit\Capability\Discovery;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;
use Mcp\Server\ClientGateway;
use Mcp\Server\RequestContext;
use Mcp\Tests\Unit\Fixtures\Enum\BackedIntEnum;
use Mcp\Tests\Unit\Fixtures\Enum\BackedStringEnum;
use Mcp\Tests\Unit\Fixtures\Enum\UnitEnum;
@ -519,6 +521,13 @@ class SchemaGeneratorFixture
{
}
/**
* @param string $query The search query
*/
public function withInjectableParameters(string $query, ClientGateway $gateway, RequestContext $context, int $limit = 10): void
{
}
// ===== OUTPUT SCHEMA FIXTURES =====
#[McpTool(
outputSchema: [

View file

@ -413,6 +413,17 @@ final class SchemaGeneratorTest extends TestCase
$this->assertEquals(['inferredParam'], $schema['required']);
}
public function testExcludesInjectableParameterTypesFromSchema(): void
{
$method = new \ReflectionMethod(SchemaGeneratorFixture::class, 'withInjectableParameters');
$schema = $this->schemaGenerator->generate($method);
$this->assertArrayNotHasKey('gateway', $schema['properties']);
$this->assertArrayNotHasKey('context', $schema['properties']);
$this->assertEquals(['type' => 'string', 'description' => 'The search query'], $schema['properties']['query']);
$this->assertEquals(['type' => 'integer', 'default' => 10], $schema['properties']['limit']);
$this->assertEquals(['query'], $schema['required']);
}
public static function methodsWithForbiddenParameter(): array
{
return [

View file

@ -108,6 +108,32 @@ final class ReferenceHandlerTest extends TestCase
$this->assertInstanceOf(ClientGateway::class, $resourceHandler->receivedGateway);
}
public function testHandleInjectsClientGatewayIntoReflectedHandler(): void
{
$session = $this->createMock(SessionInterface::class);
$session->method('getId')->willReturn(Uuid::v4());
$handler = new class {
public ?ClientGateway $receivedGateway = null;
public function search(string $query, ClientGateway $gateway): string
{
$this->receivedGateway = $gateway;
return 'found: '.$query;
}
};
$result = (new ReferenceHandler())->handle(new ElementReference([$handler, 'search']), [
'_session' => $session,
'_request' => new \stdClass(),
'query' => 'foo',
]);
$this->assertSame('found: foo', $result);
$this->assertInstanceOf(ClientGateway::class, $handler->receivedGateway);
}
public function testHandleStillReflectsOrdinaryClosuresAndDoesNotInjectArgumentBag(): void
{
$session = $this->createMock(SessionInterface::class);