Skip to content

Commit 348028b

Browse files
feat(tests): Comprehensive test suite refactor and new integration tests
- Complete refactor of unit tests for all `src/` components. - New robust integration tests for the transports - Addressed bugs discovered when adding integration tests
1 parent f9ccb59 commit 348028b

File tree

107 files changed

+7742
-4276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+7742
-4276
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"phpdocumentor/reflection-docblock": "^5.6",
1818
"psr/clock": "^1.0",
1919
"psr/container": "^1.0 || ^2.0",
20-
"psr/event-dispatcher": "^1.0",
2120
"psr/log": "^1.0 || ^2.0 || ^3.0",
2221
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
2322
"react/event-loop": "^1.5",
@@ -31,6 +30,7 @@
3130
"mockery/mockery": "^1.6",
3231
"pestphp/pest": "^2.36.0|^3.5.0",
3332
"react/async": "^4.0",
33+
"react/child-process": "^0.6.6",
3434
"symfony/var-dumper": "^6.4.11|^7.1.5"
3535
},
3636
"suggest": {

src/Contracts/IdGeneratorInterface.php

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/Dispatcher.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
use PhpMcp\Server\Configuration;
2626
use PhpMcp\Server\Contracts\SessionInterface;
2727
use PhpMcp\Server\Exception\McpServerException;
28-
use PhpMcp\Server\JsonRpc\Contents\TextContent;
28+
use PhpMcp\Schema\Content\TextContent;
2929
use PhpMcp\Schema\Result\CallToolResult;
3030
use PhpMcp\Schema\Result\CompletionCompleteResult;
3131
use PhpMcp\Schema\Result\EmptyResult;

src/Elements/RegisteredElement.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ protected function prepareArguments(object $instance, array $arguments): array
4343
$finalArgs = [];
4444

4545
foreach ($reflectionMethod->getParameters() as $parameter) {
46+
// TODO: Handle variadic parameters.
4647
$paramName = $parameter->getName();
4748
$paramPosition = $parameter->getPosition();
4849

src/Elements/RegisteredPrompt.php

Lines changed: 202 additions & 109 deletions
Large diffs are not rendered by default.

src/Elements/RegisteredResource.php

Lines changed: 91 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpMcp\Schema\Content\TextResourceContents;
1111
use PhpMcp\Schema\Resource;
1212
use Psr\Container\ContainerInterface;
13+
use Throwable;
1314

1415
class RegisteredResource extends RegisteredElement
1516
{
@@ -44,14 +45,14 @@ public function read(ContainerInterface $container, string $uri): array
4445
*
4546
* @param mixed $readResult The raw result from the resource handler method.
4647
* @param string $uri The URI of the resource that was read.
47-
* @param ?string $defaultMimeType The default MIME type from the ResourceDefinition.
48+
* @param ?string $mimeType The MIME type from the ResourceDefinition.
4849
* @return array<TextResourceContents|BlobResourceContents> Array of ResourceContents objects.
4950
*
5051
* @throws \RuntimeException If the result cannot be formatted.
5152
*
5253
* Supported result types:
53-
* - EmbeddedResource: Used as-is
54-
* - ResourceContent: Embedded resource is extracted
54+
* - ResourceContent: Used as-is
55+
* - EmbeddedResource: Resource is extracted from the EmbeddedResource
5556
* - string: Converted to text content with guessed or provided MIME type
5657
* - stream resource: Read and converted to blob with provided MIME type
5758
* - array with 'blob' key: Used as blob content
@@ -60,7 +61,7 @@ public function read(ContainerInterface $container, string $uri): array
6061
* - array: Converted to JSON if MIME type is application/json or contains 'json'
6162
* For other MIME types, will try to convert to JSON with a warning
6263
*/
63-
protected function formatResult(mixed $readResult, string $uri, ?string $defaultMimeType): array
64+
protected function formatResult(mixed $readResult, string $uri, ?string $mimeType): array
6465
{
6566
if ($readResult instanceof ResourceContents) {
6667
return [$readResult];
@@ -70,16 +71,54 @@ protected function formatResult(mixed $readResult, string $uri, ?string $default
7071
return [$readResult->resource];
7172
}
7273

73-
if (is_array($readResult) && ! empty($readResult) && $readResult[array_key_first($readResult)] instanceof ResourceContents) {
74-
return $readResult;
75-
}
74+
if (is_array($readResult)) {
75+
if (empty($readResult)) {
76+
return [TextResourceContents::make($uri, 'application/json', '[]')];
77+
}
7678

77-
if (is_array($readResult) && ! empty($readResult) && $readResult[array_key_first($readResult)] instanceof EmbeddedResource) {
78-
return array_map(fn($item) => $item->resource, $readResult);
79+
$allAreResourceContents = true;
80+
$hasResourceContents = false;
81+
$allAreEmbeddedResource = true;
82+
$hasEmbeddedResource = false;
83+
84+
foreach ($readResult as $item) {
85+
if ($item instanceof ResourceContents) {
86+
$hasResourceContents = true;
87+
$allAreEmbeddedResource = false;
88+
} elseif ($item instanceof EmbeddedResource) {
89+
$hasEmbeddedResource = true;
90+
$allAreResourceContents = false;
91+
} else {
92+
$allAreResourceContents = false;
93+
$allAreEmbeddedResource = false;
94+
}
95+
}
96+
97+
if ($allAreResourceContents && $hasResourceContents) {
98+
return $readResult;
99+
}
100+
101+
if ($allAreEmbeddedResource && $hasEmbeddedResource) {
102+
return array_map(fn($item) => $item->resource, $readResult);
103+
}
104+
105+
if ($hasResourceContents || $hasEmbeddedResource) {
106+
$result = [];
107+
foreach ($readResult as $item) {
108+
if ($item instanceof ResourceContents) {
109+
$result[] = $item;
110+
} elseif ($item instanceof EmbeddedResource) {
111+
$result[] = $item->resource;
112+
} else {
113+
$result = array_merge($result, $this->formatResult($item, $uri, $mimeType));
114+
}
115+
}
116+
return $result;
117+
}
79118
}
80119

81120
if (is_string($readResult)) {
82-
$mimeType = $defaultMimeType ?? $this->guessMimeTypeFromString($readResult);
121+
$mimeType = $mimeType ?? $this->guessMimeTypeFromString($readResult);
83122

84123
return [TextResourceContents::make($uri, $mimeType, $readResult)];
85124
}
@@ -88,7 +127,7 @@ protected function formatResult(mixed $readResult, string $uri, ?string $default
88127
$result = BlobResourceContents::fromStream(
89128
$uri,
90129
$readResult,
91-
$defaultMimeType ?? 'application/octet-stream'
130+
$mimeType ?? 'application/octet-stream'
92131
);
93132

94133
@fclose($readResult);
@@ -97,36 +136,40 @@ protected function formatResult(mixed $readResult, string $uri, ?string $default
97136
}
98137

99138
if (is_array($readResult) && isset($readResult['blob']) && is_string($readResult['blob'])) {
100-
$mimeType = $readResult['mimeType'] ?? $defaultMimeType ?? 'application/octet-stream';
139+
$mimeType = $readResult['mimeType'] ?? $mimeType ?? 'application/octet-stream';
101140

102141
return [BlobResourceContents::make($uri, $mimeType, $readResult['blob'])];
103142
}
104143

105144
if (is_array($readResult) && isset($readResult['text']) && is_string($readResult['text'])) {
106-
$mimeType = $readResult['mimeType'] ?? $defaultMimeType ?? 'text/plain';
145+
$mimeType = $readResult['mimeType'] ?? $mimeType ?? 'text/plain';
107146

108147
return [TextResourceContents::make($uri, $mimeType, $readResult['text'])];
109148
}
110149

111150
if ($readResult instanceof \SplFileInfo && $readResult->isFile() && $readResult->isReadable()) {
112-
return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $defaultMimeType)];
151+
if ($mimeType && str_contains(strtolower($mimeType), 'text')) {
152+
return [TextResourceContents::make($uri, $mimeType, file_get_contents($readResult->getPathname()))];
153+
}
154+
155+
return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType)];
113156
}
114157

115158
if (is_array($readResult)) {
116-
if ($defaultMimeType && (str_contains(strtolower($defaultMimeType), 'json') ||
117-
$defaultMimeType === 'application/json')) {
159+
if ($mimeType && (str_contains(strtolower($mimeType), 'json') ||
160+
$mimeType === 'application/json')) {
118161
try {
119162
$jsonString = json_encode($readResult, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
120163

121-
return [TextResourceContents::make($uri, $defaultMimeType, $jsonString)];
164+
return [TextResourceContents::make($uri, $mimeType, $jsonString)];
122165
} catch (\JsonException $e) {
123166
throw new \RuntimeException("Failed to encode array as JSON for URI '{$uri}': {$e->getMessage()}");
124167
}
125168
}
126169

127170
try {
128171
$jsonString = json_encode($readResult, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
129-
$mimeType = 'application/json';
172+
$mimeType = $mimeType ?? 'application/json';
130173

131174
return [TextResourceContents::make($uri, $mimeType, $jsonString)];
132175
} catch (\JsonException $e) {
@@ -141,24 +184,48 @@ protected function formatResult(mixed $readResult, string $uri, ?string $default
141184
private function guessMimeTypeFromString(string $content): string
142185
{
143186
$trimmed = ltrim($content);
187+
144188
if (str_starts_with($trimmed, '<') && str_ends_with(rtrim($content), '>')) {
145-
// Looks like HTML or XML? Prefer text/plain unless sure.
146-
if (stripos($trimmed, '<html') !== false) {
189+
if (str_contains($trimmed, '<html')) {
147190
return 'text/html';
148191
}
149-
if (stripos($trimmed, '<?xml') !== false) {
192+
if (str_contains($trimmed, '<?xml')) {
150193
return 'application/xml';
151-
} // or text/xml
194+
}
152195

153-
return 'text/plain'; // Default for tag-like structures
196+
return 'text/plain';
154197
}
198+
155199
if (str_starts_with($trimmed, '{') && str_ends_with(rtrim($content), '}')) {
156200
return 'application/json';
157201
}
202+
158203
if (str_starts_with($trimmed, '[') && str_ends_with(rtrim($content), ']')) {
159204
return 'application/json';
160205
}
161206

162-
return 'text/plain'; // Default
207+
return 'text/plain';
208+
}
209+
210+
public function toArray(): array
211+
{
212+
return [
213+
'schema' => $this->schema->toArray(),
214+
...parent::toArray(),
215+
];
216+
}
217+
218+
public static function fromArray(array $data): self|false
219+
{
220+
try {
221+
return new self(
222+
Resource::fromArray($data['schema']),
223+
$data['handlerClass'],
224+
$data['handlerMethod'],
225+
$data['isManual'] ?? false,
226+
);
227+
} catch (Throwable $e) {
228+
return false;
229+
}
163230
}
164231
}

0 commit comments

Comments
 (0)