Skip to content
Closed
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
4 changes: 2 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="inspector">
<directory>tests/Inspector</directory>
<testsuite name="application">
<directory>tests/Application</directory>
</testsuite>
</testsuites>

Expand Down
172 changes: 172 additions & 0 deletions tests/Application/ApplicationTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

/*
* 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\Application;

use Mcp\Schema\JsonRpc\MessageInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;

abstract class ApplicationTestCase extends TestCase
{
/**
* @param list<string> $messages
* @param array<string,string> $env
*
* @return array<string, array<string, array<string,mixed>>>
*/
protected function runServer(array $messages, float $timeout = 5.0, array $env = []): array
{
if (0 === \count($messages)) {
return [];
}

$process = new Process([
'php',
$this->getServerScript(),
], \dirname(__DIR__, 2), [] === $env ? null : $env, null, $timeout);

$process->setInput($this->formatInput($messages));
$process->mustRun();

return $this->decodeJsonLines($process->getOutput());
}

abstract protected function getServerScript(): string;

/**
* @param mixed[] $params
*
* @throws \JsonException
*/
protected function jsonRequest(string $method, ?array $params = null, ?string $id = null): string
{
$payload = [
'jsonrpc' => MessageInterface::JSONRPC_VERSION,
'id' => $id,
'method' => $method,
];

if (null !== $params) {
$payload['params'] = $params;
}

return (string) json_encode($payload, \JSON_THROW_ON_ERROR);
}

/**
* @param list<string> $messages
*/
private function formatInput(array $messages): string
{
return implode("\n", $messages)."\n";
}

/**
* @return array<string, array<string, array<string,mixed>>>
*/
private function decodeJsonLines(string $output): array
{
$output = trim($output);
$responses = [];

if ('' === $output) {
return $responses;
}

foreach (preg_split('/\R+/', $output) as $line) {
if ('' === $line) {
continue;
}

try {
$decoded = json_decode($line, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
continue;
}

if (!\is_array($decoded)) {
continue;
}

$id = $decoded['id'] ?? null;

if (\is_string($id) || \is_int($id)) {
$responses[(string) $id] = $decoded;
}
}

return $responses;
}

protected function getSnapshotFilePath(string $method): string
{
$className = substr(static::class, strrpos(static::class, '\\') + 1);

return __DIR__.'/snapshots/'.$className.'-'.str_replace('/', '_', $method).'.json';
}

/**
* @return array<string,mixed>
*
* @throws \JsonException
*/
protected function loadSnapshot(string $method): array
{
$path = $this->getSnapshotFilePath($method);

$contents = file_get_contents($path);
$this->assertNotFalse($contents, 'Failed to read snapshot: '.$path);

return json_decode($contents, true, 512, \JSON_THROW_ON_ERROR);
}

/**
* @param array<string, array<string, mixed>> $response
*
* @throws \JsonException
*/
protected function assertResponseMatchesSnapshot(array $response, string $method): void
{
$this->assertArrayHasKey('result', $response);
$actual = $response['result'];

$expected = $this->loadSnapshot($method);

$this->assertEquals($expected, $actual, 'Response payload does not match snapshot '.$this->getSnapshotFilePath($method));
}

/**
* @param array<string, array<string, mixed>> $capabilities
* @param string[] $clientInfo
*
* @throws \JsonException
*/
protected function initializeMessage(
?string $id = null,
string $protocolVersion = MessageInterface::PROTOCOL_VERSION,
array $capabilities = [],
array $clientInfo = [
'name' => 'test-suite',
'version' => '1.0.0',
],
): string {
$id ??= uniqid();

return $this->jsonRequest('initialize', [
'protocolVersion' => $protocolVersion,
'capabilities' => $capabilities,
'clientInfo' => $clientInfo,
], $id);
}
}
107 changes: 107 additions & 0 deletions tests/Application/ManualStdioExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

/*
* 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\Application;

final class ManualStdioExampleTest extends ApplicationTestCase
{
/**
* @throws \JsonException
*/
public function testInitializeResponseMatchesSnapshot(): void
{
$initializeId = uniqid('i_');

$responses = $this->runServer([
$this->initializeMessage($initializeId),
]);

$this->assertArrayHasKey($initializeId, $responses);

$initialize = $responses[$initializeId];
$this->assertResponseMatchesSnapshot($initialize, 'initialize');
}

/**
* @throws \JsonException
*/
public function testToolsListMatchesSnapshot(): void
{
$toolsListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('tools/list', id: $toolsListId),
]);

$this->assertArrayHasKey($toolsListId, $responses);
$toolsList = $responses[$toolsListId];
$this->assertResponseMatchesSnapshot($toolsList, 'tools/list');
}

/**
* @throws \JsonException
*/
public function testPromptsListMatchesSnapshot(): void
{
$promptsListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('prompts/list', id: $promptsListId),
]);

$this->assertArrayHasKey($promptsListId, $responses);
$promptsList = $responses[$promptsListId];
$this->assertResponseMatchesSnapshot($promptsList, 'prompts/list');
}

/**
* @throws \JsonException
*/
public function testResourcesListMatchesSnapshot(): void
{
$resourcesListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('resources/list', id: $resourcesListId),
]);

$this->assertArrayHasKey($resourcesListId, $responses);
$resourcesList = $responses[$resourcesListId];
$this->assertResponseMatchesSnapshot($resourcesList, 'resources/list');
}

/**
* @throws \JsonException
*/
public function testResourceTemplatesListMatchesSnapshot(): void
{
$templatesListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('resources/templates/list', id: $templatesListId),
]);

$this->assertArrayHasKey($templatesListId, $responses);
$templatesList = $responses[$templatesListId];
$this->assertResponseMatchesSnapshot($templatesList, 'resources/templates/list');
}

protected function getServerScript(): string
{
return \dirname(__DIR__, 2).'/examples/stdio-explicit-registration/server.php';
}
}
92 changes: 92 additions & 0 deletions tests/Application/StdioCalculatorExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/*
* 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 Application;

use Mcp\Tests\Application\ApplicationTestCase;

final class StdioCalculatorExampleTest extends ApplicationTestCase
{
/**
* @throws \JsonException
*/
public function testToolsListMatchesSnapshot(): void
{
$toolsListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('tools/list', id: $toolsListId),
]);

$this->assertArrayHasKey($toolsListId, $responses);
$toolsList = $responses[$toolsListId];
$this->assertResponseMatchesSnapshot($toolsList, 'tools/list');
}

/**
* @throws \JsonException
*/
public function testPromptsListMatchesSnapshot(): void
{
$promptsListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('prompts/list', id: $promptsListId),
]);

$this->assertArrayHasKey($promptsListId, $responses);
$promptsList = $responses[$promptsListId];
$this->assertResponseMatchesSnapshot($promptsList, 'prompts/list');
}

/**
* @throws \JsonException
*/
public function testResourcesListMatchesSnapshot(): void
{
$resourcesListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('resources/list', id: $resourcesListId),
]);

$this->assertArrayHasKey($resourcesListId, $responses);
$resourcesList = $responses[$resourcesListId];
$this->assertResponseMatchesSnapshot($resourcesList, 'resources/list');
}

/**
* @throws \JsonException
*/
public function testResourceTemplatesListMatchesSnapshot(): void
{
$templatesListId = uniqid('t_');

$responses = $this->runServer([
$this->initializeMessage(),
$this->jsonRequest('resources/templates/list', id: $templatesListId),
]);

$this->assertArrayHasKey($templatesListId, $responses);
$templatesList = $responses[$templatesListId];
$this->assertResponseMatchesSnapshot($templatesList, 'resources/templates/list');
}

protected function getServerScript(): string
{
return \dirname(__DIR__, 2).'/examples/stdio-discovery-calculator/server.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"capabilities": {
"prompts": {},
"resources": {},
"tools": {},
"completions": {}
},
"serverInfo": {
"name": "Manual Reg Server",
"version": "1.0.0"
},
"protocolVersion": "2025-06-18"
}
Loading