fix: use Docker PHP-FPM to support sampling in conformance tests (#219)

This commit is contained in:
Kyrian Obikwelu 2026-01-02 15:59:56 +01:00 • committed by GitHub
commit 4c3e5e61c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 144 additions and 40 deletions

View file

@ -0,0 +1,131 @@
<?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\Conformance;
use Mcp\Schema\Content\Content;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\PromptMessage;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Enum\Role;
use Mcp\Server\Protocol;
use Mcp\Server\RequestContext;
final class Elements
{
public const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
public const TEST_AUDIO_BASE64 = 'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=';
/**
* @return Content[]
*/
public function toolMultipleTypes(): array
{
return [
new TextContent('Multiple content types test:'),
new ImageContent(self::TEST_IMAGE_BASE64, 'image/png'),
EmbeddedResource::fromText(
'test://mixed-content-resource',
'{ "test" = "data", "value" = 123 }',
'application/json',
),
];
}
public function toolWithLogging(RequestContext $context): string
{
$logger = $context->getClientLogger();
$logger->info('Tool execution started');
$logger->info('Tool processing data');
$logger->info('Tool execution completed');
return 'Tool with logging executed successfully';
}
public function toolWithProgress(RequestContext $context): ?string
{
$client = $context->getClientGateway();
$client->progress(0, 100, 'Completed step 0 of 100');
$client->progress(50, 100, 'Completed step 50 of 100');
$client->progress(100, 100, 'Completed step 100 of 100');
$meta = $context->getSession()->get(Protocol::SESSION_ACTIVE_REQUEST_META, []);
return $meta['progressToken'] ?? null;
}
/**
* @param string $prompt The prompt to send to the LLM
*/
public function toolWithSampling(RequestContext $context, string $prompt): string
{
$result = $context->getClientGateway()->sample($prompt, 100);
return \sprintf(
'LLM response: %s',
$result->content instanceof TextContent ? trim((string) $result->content->text) : ''
);
}
public function resourceTemplate(string $id): TextResourceContents
{
return new TextResourceContents(
uri: 'test://template/{id}/data',
mimeType: 'application/json',
text: json_encode([
'id' => $id,
'templateTest' => true,
'data' => \sprintf('Data for ID: %s', $id),
]),
);
}
/**
* @param string $arg1 First test argument
* @param string $arg2 Second test argument
*
* @return PromptMessage[]
*/
public function promptWithArguments(string $arg1, string $arg2): array
{
return [
new PromptMessage(Role::User, new TextContent(\sprintf('Prompt with arguments: arg1="%s", arg2="%s"', $arg1, $arg2))),
];
}
/**
* @param string $resourceUri URI of the resource to embed
*
* @return PromptMessage[]
*/
public function promptWithEmbeddedResource(string $resourceUri): array
{
return [
new PromptMessage(Role::User, EmbeddedResource::fromText($resourceUri, 'Embedded resource content for testing.')),
new PromptMessage(Role::User, new TextContent('Please process the embedded resource above.')),
];
}
/**
* @return PromptMessage[]
*/
public function promptWithImage(): array
{
return [
new PromptMessage(Role::User, new ImageContent(self::TEST_IMAGE_BASE64, 'image/png')),
new PromptMessage(Role::User, new TextContent('Please analyze the image above.')),
];
}
}

View file

@ -0,0 +1,33 @@
<?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\Conformance;
use Psr\Log\AbstractLogger;
final class FileLogger extends AbstractLogger
{
public function __construct(
private readonly string $filePath,
private readonly bool $debug = false,
) {
}
public function log($level, mixed $message, array $context = []): void
{
if (!$this->debug && 'debug' === $level) {
return;
}
$logMessage = \sprintf("[%s] %s\n", strtoupper($level), $message);
file_put_contents($this->filePath, $logMessage, \FILE_APPEND);
}
}

View file

@ -0,0 +1,25 @@
services:
nginx:
image: nginx:1.26-alpine
ports:
- "8000:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ../../..:/app:ro
depends_on:
- php-fpm
networks:
- mcp-net
php-fpm:
image: php:8.4-fpm-alpine
volumes:
- ../../..:/app:ro
- ../sessions:/app/tests/Conformance/sessions
- ../logs:/app/tests/Conformance/logs
working_dir: /app
networks:
- mcp-net
networks:
mcp-net:

View file

@ -0,0 +1,15 @@
server {
listen 80;
server_name localhost;
root /app;
location / {
try_files $uri /tests/Conformance/server.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass php-fpm:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

View file

@ -0,0 +1,65 @@
<?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__, 2).'/vendor/autoload.php';
use Http\Discovery\Psr17Factory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Mcp\Schema\Content\AudioContent;
use Mcp\Schema\Content\EmbeddedResource;
use Mcp\Schema\Content\ImageContent;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
use Mcp\Server;
use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport;
use Mcp\Tests\Conformance\Elements;
use Mcp\Tests\Conformance\FileLogger;
chdir(__DIR__);
$logger = new FileLogger(__DIR__.'/logs/conformance.log', true);
$psr17Factory = new Psr17Factory();
$request = $psr17Factory->createServerRequestFromGlobals();
$transport = new StreamableHttpTransport($request, logger: $logger);
$server = Server::builder()
->setServerInfo('mcp-conformance-test-server', '1.0.0')
->setSession(new FileSessionStore(__DIR__.'/sessions'))
->setLogger($logger)
// Tools
->addTool(fn () => 'This is a simple text response for testing.', 'test_simple_text', 'Tests simple text content response')
->addTool(fn () => new ImageContent(Elements::TEST_IMAGE_BASE64, 'image/png'), 'test_image_content', 'Tests image content response')
->addTool(fn () => new AudioContent(Elements::TEST_AUDIO_BASE64, 'audio/wav'), 'test_audio_content', 'Tests audio content response')
->addTool(fn () => EmbeddedResource::fromText('test://embedded-resource', 'This is an embedded resource content.'), 'test_embedded_resource', 'Tests embedded resource content response')
->addTool([Elements::class, 'toolMultipleTypes'], 'test_multiple_content_types', 'Tests response with multiple content types')
->addTool([Elements::class, 'toolWithLogging'], 'test_tool_with_logging', 'Tests tool that emits log messages')
->addTool([Elements::class, 'toolWithProgress'], 'test_tool_with_progress', 'Tests tool that reports progress notifications')
->addTool([Elements::class, 'toolWithSampling'], 'test_sampling', 'Tests server-initiated sampling')
->addTool(fn () => CallToolResult::error([new TextContent('This tool intentionally returns an error for testing')]), 'test_error_handling', 'Tests error response handling')
// Resources
->addResource(fn () => 'This is the content of the static text resource.', 'test://static-text', 'static-text', 'A static text resource for testing')
->addResource(fn () => fopen('data://image/png;base64,'.Elements::TEST_IMAGE_BASE64, 'r'), 'test://static-binary', 'static-binary', 'A static binary resource (image) for testing')
->addResourceTemplate([Elements::class, 'resourceTemplate'], 'test://template/{id}/data', 'template', 'A resource template with parameter substitution', 'application/json')
// TODO: Handler for resources/subscribe and resources/unsubscribe
->addResource(fn () => 'Watched resource content', 'test://watched-resource', 'watched-resource', 'A resource that can be watched')
// Prompts
->addPrompt(fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], 'test_simple_prompt', 'A simple prompt without arguments')
->addPrompt([Elements::class, 'promptWithArguments'], 'test_prompt_with_arguments', 'A prompt with required arguments')
->addPrompt([Elements::class, 'promptWithEmbeddedResource'], 'test_prompt_with_embedded_resource', 'A prompt that includes an embedded resource')
->addPrompt([Elements::class, 'promptWithImage'], 'test_prompt_with_image', 'A prompt that includes image content')
->build();
$response = $server->run($transport);
(new SapiEmitter())->emit($response);