Skip to content

Commit 2f29b1d

Browse files
authored
[Server][Conformance] Kicking off adoption of conformance testing (#181)
1 parent 7f02285 commit 2f29b1d

File tree

6 files changed

+236
-0
lines changed

6 files changed

+236
-0
lines changed

.github/workflows/pipeline.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,46 @@ jobs:
6969
- name: Tests
7070
run: vendor/bin/phpunit --testsuite=inspector
7171

72+
conformance:
73+
runs-on: ubuntu-latest
74+
steps:
75+
- name: Checkout
76+
uses: actions/checkout@v5
77+
78+
- name: Setup PHP
79+
uses: shivammathur/setup-php@v2
80+
with:
81+
php-version: '8.4'
82+
coverage: "none"
83+
84+
- name: Setup Node
85+
uses: actions/setup-node@v5
86+
with:
87+
node-version: '22'
88+
89+
- name: Install Composer
90+
uses: "ramsey/composer-install@v3"
91+
92+
- name: Run server
93+
run: php -S localhost:8000 examples/server/conformance/server.php &
94+
95+
- name: Wait for server to start
96+
run: sleep 5
97+
98+
- name: Tests
99+
run: |
100+
exit_code=0
101+
OUTPUT=$(npx @modelcontextprotocol/conformance server --url http://localhost:8000/) || exit_code=1
102+
echo "$OUTPUT"
103+
104+
# Example: "Total: 3 passed, 16 failed"
105+
passedTests=$(echo "$OUTPUT" | sed -nE 's/.*Total: ([0-9]+) passed.*/\1/p')
106+
passedTests=${passedTests:-0}
107+
108+
REQUIRED_TESTS_TO_PASS=20
109+
echo "Required tests to pass: $REQUIRED_TESTS_TO_PASS"
110+
[ "$passedTests" -ge "$REQUIRED_TESTS_TO_PASS" ] || exit $exit_code
111+
72112
qa:
73113
runs-on: ubuntu-latest
74114
steps:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ vendor
66
examples/**/dev.log
77
examples/**/cache
88
examples/**/sessions
9+
results

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ unit-tests:
2121
inspector-tests:
2222
vendor/bin/phpunit --testsuite=inspector
2323

24+
conformance-server:
25+
php -S localhost:8000 examples/server/conformance/server.php
26+
27+
conformance-tests:
28+
npx @modelcontextprotocol/conformance server --url http://localhost:8000/
29+
2430
coverage:
2531
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=unit --coverage-html=coverage
2632

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
5757
"Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
5858
"Mcp\\Example\\Server\\ComplexToolSchema\\": "examples/server/complex-tool-schema/",
59+
"Mcp\\Example\\Server\\Conformance\\": "examples/server/conformance/",
5960
"Mcp\\Example\\Server\\CustomDependencies\\": "examples/server/custom-dependencies/",
6061
"Mcp\\Example\\Server\\CustomMethodHandlers\\": "examples/server/custom-method-handlers/",
6162
"Mcp\\Example\\Server\\DiscoveryCalculator\\": "examples/server/discovery-calculator/",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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\Example\Server\Conformance;
13+
14+
use Mcp\Schema\Content\Content;
15+
use Mcp\Schema\Content\EmbeddedResource;
16+
use Mcp\Schema\Content\ImageContent;
17+
use Mcp\Schema\Content\PromptMessage;
18+
use Mcp\Schema\Content\TextContent;
19+
use Mcp\Schema\Content\TextResourceContents;
20+
use Mcp\Schema\Enum\Role;
21+
use Mcp\Server\Protocol;
22+
use Mcp\Server\RequestContext;
23+
24+
final class Elements
25+
{
26+
// Sample base64 encoded 1x1 red PNG pixel for testing
27+
public const TEST_IMAGE_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
28+
// Sample base64 encoded minimal WAV file for testing
29+
public const TEST_AUDIO_BASE64 = 'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=';
30+
31+
/**
32+
* @return Content[]
33+
*/
34+
public function toolMultipleTypes(): array
35+
{
36+
return [
37+
new TextContent('Multiple content types test:'),
38+
new ImageContent(self::TEST_IMAGE_BASE64, 'image/png'),
39+
EmbeddedResource::fromText(
40+
'test://mixed-content-resource',
41+
'{ "test" = "data", "value" = 123 }',
42+
'application/json',
43+
),
44+
];
45+
}
46+
47+
public function toolWithLogging(RequestContext $context): string
48+
{
49+
$logger = $context->getClientLogger();
50+
51+
$logger->info('Tool execution started');
52+
$logger->info('Tool processing data');
53+
$logger->info('Tool execution completed');
54+
55+
return 'Tool with logging executed successfully';
56+
}
57+
58+
public function toolWithProgress(RequestContext $context): ?string
59+
{
60+
$client = $context->getClientGateway();
61+
62+
$client->progress(0, 100, 'Completed step 0 of 100');
63+
$client->progress(50, 100, 'Completed step 0 of 100');
64+
$client->progress(100, 100, 'Completed step 0 of 100');
65+
66+
$meta = $context->getSession()->get(Protocol::SESSION_ACTIVE_REQUEST_META, []);
67+
68+
return $meta['progressToken'] ?? null;
69+
}
70+
71+
/**
72+
* @param string $prompt The prompt to send to the LLM
73+
*/
74+
public function toolWithSampling(RequestContext $context, string $prompt): string
75+
{
76+
$result = $context->getClientGateway()->sample($prompt, 100);
77+
78+
return \sprintf('LLM response: %s', $result->content instanceof TextContent ? trim((string) $result->content->text) : '');
79+
}
80+
81+
public function resourceTemplate(string $id): TextResourceContents
82+
{
83+
return new TextResourceContents(
84+
uri: 'test://template/{id}/data',
85+
mimeType: 'application/json',
86+
text: json_encode([
87+
'id' => $id,
88+
'templateTest' => true,
89+
'data' => \sprintf('Data for ID: %s', $id),
90+
]),
91+
);
92+
}
93+
94+
/**
95+
* @param string $arg1 First test argument
96+
* @param string $arg2 Second test argument
97+
*
98+
* @return PromptMessage[]
99+
*/
100+
public function promptWithArguments(string $arg1, string $arg2): array
101+
{
102+
return [
103+
new PromptMessage(Role::User, new TextContent(\sprintf('Prompt with arguments: arg1="%s", arg2="%s"', $arg1, $arg2))),
104+
];
105+
}
106+
107+
/**
108+
* @param string $resourceUri URI of the resource to embed
109+
*
110+
* @return PromptMessage[]
111+
*/
112+
public function promptWithEmbeddedResource(string $resourceUri): array
113+
{
114+
return [
115+
new PromptMessage(Role::User, EmbeddedResource::fromText($resourceUri, 'Embedded resource content for testing.')),
116+
new PromptMessage(Role::User, new TextContent('Please process the embedded resource above.')),
117+
];
118+
}
119+
120+
/**
121+
* @return PromptMessage[]
122+
*/
123+
public function promptWithImage(): array
124+
{
125+
return [
126+
new PromptMessage(Role::User, new ImageContent(self::TEST_IMAGE_BASE64, 'image/png')),
127+
new PromptMessage(Role::User, new TextContent('Please analyze the image above.')),
128+
];
129+
}
130+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
require_once dirname(__DIR__).'/bootstrap.php';
13+
chdir(__DIR__);
14+
15+
use Mcp\Example\Server\Conformance\Elements;
16+
use Mcp\Schema\Content\AudioContent;
17+
use Mcp\Schema\Content\EmbeddedResource;
18+
use Mcp\Schema\Content\ImageContent;
19+
use Mcp\Schema\Content\TextContent;
20+
use Mcp\Schema\Result\CallToolResult;
21+
use Mcp\Server;
22+
use Mcp\Server\Session\FileSessionStore;
23+
24+
logger()->info('Starting MCP Custom Dependencies Server...');
25+
26+
$server = Server::builder()
27+
->setServerInfo('mcp-conformance-test-server', '1.0.0')
28+
->setSession(new FileSessionStore(__DIR__.'/sessions'))
29+
->setLogger(logger())
30+
// Tools
31+
->addTool(fn () => 'This is a simple text response for testing.', 'test_simple_text', 'Tests simple text content response')
32+
->addTool(fn () => new ImageContent(Elements::TEST_IMAGE_BASE64, 'image/png'), 'test_image_content', 'Tests image content response')
33+
->addTool(fn () => new AudioContent(Elements::TEST_AUDIO_BASE64, 'audio/wav'), 'test_audio_content', 'Tests audio content response')
34+
->addTool(fn () => EmbeddedResource::fromText('test://embedded-resource', 'This is an embedded resource content.'), 'test_embedded_resource', 'Tests embedded resource content response')
35+
->addTool([Elements::class, 'toolMultipleTypes'], 'test_multiple_content_types', 'Tests response with multiple content types (text, image, resource)')
36+
->addTool([Elements::class, 'toolWithLogging'], 'test_tool_with_logging', 'Tests tool that emits log messages during execution')
37+
->addTool([Elements::class, 'toolWithProgress'], 'test_tool_with_progress', 'Tests tool that reports progress notifications')
38+
->addTool(fn () => CallToolResult::error([new TextContent('This tool intentionally returns an error for testing')]), 'test_error_handling', 'Tests error response handling')
39+
// TODO: Sampling gets stuck
40+
// ->addTool([Elements::class, 'toolWithSampling'], 'test_sampling', 'Tests server-initiated sampling (LLM completion request)')
41+
// Resources
42+
->addResource(fn () => 'This is the content of the static text resource.', 'test://static-text', 'static-text', 'A static text resource for testing')
43+
->addResource(fn () => ''/* TODO: Missing Support for Binary? */, 'test://static-binary', 'static-binary', 'A static binary resource (image) for testing')
44+
->addResourceTemplate([Elements::class, 'resourceTemplate'], 'test://template/{id}/data', 'template', 'A resource template with parameter substitution', 'application/json')
45+
// TODO: Handler for resources/subscribe and resources/unsubscribe
46+
->addResource(fn () => 'Watched resource content', 'test://watched-resource', 'watched-resource', 'A resource that auto-updates every 3 seconds')
47+
// Prompts
48+
->addPrompt(fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], 'test_simple_prompt', 'A simple prompt without arguments')
49+
->addPrompt([Elements::class, 'promptWithArguments'], 'test_prompt_with_arguments', 'A prompt with required arguments')
50+
->addPrompt([Elements::class, 'promptWithEmbeddedResource'], 'test_prompt_with_embedded_resource', 'A prompt that includes an embedded resource')
51+
->addPrompt([Elements::class, 'promptWithImage'], 'test_prompt_with_image', 'A prompt that includes image content')
52+
->build();
53+
54+
$result = $server->run(transport());
55+
56+
logger()->info('Server listener stopped gracefully.', ['result' => $result]);
57+
58+
shutdown($result);

0 commit comments

Comments
 (0)