Skip to content

Commit 971035b

Browse files
committed
[QA] Raise PHPStan to level 8
Fixes all 330 errors the bump surfaces across src/, tests/ and examples/, without new baseline entries or ignore comments.
1 parent f39625b commit 971035b

133 files changed

Lines changed: 1027 additions & 441 deletions

File tree

Some content is hidden

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

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to `mcp/sdk` will be documented in this file.
66
-----
77

88
* [BC Break] Remove the `providerClass` argument of `#[CompletionProvider]`. Use `provider:`, which takes the same class-string and is now the first positional argument.
9+
* [BC Break] `StreamableHttpTransport::handleFiberTermination()` takes the terminated `\Fiber` as its first argument; a subclass overriding it has to accept it too.
10+
* [BC Break] Reject a `Tool` input schema whose `properties` is not an object or whose `required` is neither a list nor `null`, instead of silently replacing the member. Reject a `completion/complete` whose `argument` is missing `name` or `value`, instead of completing against an empty prefix.
911
* Add `HttpTransport::getSessionId()` to read the server-minted `Mcp-Session-Id`: a request-scoped caller can persist it and pass it back through the constructor's `$headers` on a later transport. Always `null` on `2026-07-28`, which removed protocol-level sessions.
1012

1113
0.8.0

examples/client/stdio_elicitation.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,14 @@ public function __invoke(ElicitRequest $request): ElicitResult
4343
{
4444
echo "\n[ELICIT] {$request->message}\n";
4545

46+
if (null === $request->requestedSchema) {
47+
return new ElicitResult(ElicitAction::Decline);
48+
}
49+
4650
$content = [];
4751
foreach ($request->requestedSchema->properties as $name => $definition) {
4852
$default = $this->defaultFor($definition);
49-
$label = $this->labelFor($definition);
53+
$label = $this->labelFor($definition, $name);
5054

5155
if (null !== $default) {
5256
$display = is_bool($default) ? ($default ? 'true' : 'false') : (string) $default;
@@ -76,9 +80,11 @@ private function defaultFor(object $definition): mixed
7680
};
7781
}
7882

79-
private function labelFor(AbstractSchemaDefinition $definition): string
83+
private function labelFor(AbstractSchemaDefinition $definition, string $name): string
8084
{
81-
return $definition->title;
85+
// A schema is not obliged to carry a title, and the field's own name is
86+
// a far better prompt than a blank one.
87+
return $definition->title ?? $name;
8288
}
8389

8490
private function cast(object $definition, string $input): mixed

examples/server/bootstrap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function transport(): TransportInterface
5151

5252
function shutdown(ResponseInterface|int $result): never
5353
{
54-
if ('cli' === \PHP_SAPI) {
54+
if (is_int($result)) {
5555
exit($result);
5656
}
5757

examples/server/mcp-apps/WeatherApp.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ public function getWeatherApp(): TextResourceContents
3232
prefersBorder: true,
3333
);
3434

35+
$html = file_get_contents(__DIR__.'/weather-app.html');
36+
if (false === $html) {
37+
throw new \RuntimeException('Could not read the weather app template.');
38+
}
39+
3540
return new TextResourceContents(
3641
uri: 'ui://weather-app',
3742
mimeType: McpApps::MIME_TYPE,
38-
text: file_get_contents(__DIR__.'/weather-app.html'),
43+
text: $html,
3944
meta: ['ui' => $contentMeta],
4045
);
4146
}

examples/server/oauth-microsoft/McpElements.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function listEmails(int $count = 5): array
106106
'id' => 'msg_'.uniqid(),
107107
'subject' => "Sample Email #{$i}",
108108
'from' => "sender{$i}@example.com",
109-
'receivedDateTime' => date('c', strtotime("-{$i} hours")),
109+
'receivedDateTime' => date('c', strtotime("-{$i} hours") ?: time()),
110110
], range(1, $count)),
111111
];
112112
}

phpstan.dist.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ includes:
22
- phpstan-baseline.neon
33

44
parameters:
5-
level: 6
5+
level: 8
66
paths:
77
- examples/
88
- src/

src/Capability/Attribute/Schema.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
* minLength?: int,
3030
* maxLength?: int,
3131
* pattern?: string,
32-
* minimum?: int,
33-
* maximum?: int,
34-
* exclusiveMinimum?: int,
35-
* exclusiveMaximum?: int,
32+
* minimum?: int|float,
33+
* maximum?: int|float,
34+
* exclusiveMinimum?: bool,
35+
* exclusiveMaximum?: bool,
3636
* multipleOf?: int|float,
3737
* items?: array<string, mixed>,
3838
* minItems?: int,

src/Capability/Completion/ListCompletionProvider.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
class ListCompletionProvider implements ProviderInterface
1818
{
1919
/**
20-
* @param string[] $values
20+
* @var string[]
2121
*/
22-
public function __construct(
23-
private array $values,
24-
) {
22+
private array $values;
23+
24+
/**
25+
* @param array<int|float|string> $values
26+
*/
27+
public function __construct(array $values)
28+
{
29+
$this->values = array_values(array_map(strval(...), $values));
2530
}
2631

2732
public function getCompletions(string $currentValue): array

src/Capability/Discovery/Discoverer.php

Lines changed: 66 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,13 @@
4949
*/
5050
final class Discoverer implements DiscovererInterface
5151
{
52+
private readonly DocBlockParser $docBlockParser;
53+
private readonly SchemaGeneratorInterface $schemaGenerator;
54+
5255
public function __construct(
5356
private readonly LoggerInterface $logger = new NullLogger(),
54-
private ?DocBlockParser $docBlockParser = null,
55-
private ?SchemaGeneratorInterface $schemaGenerator = null,
57+
?DocBlockParser $docBlockParser = null,
58+
?SchemaGeneratorInterface $schemaGenerator = null,
5659
) {
5760
if (!class_exists(Finder::class)) {
5861
throw new RuntimeException('File-based discovery requires symfony/finder. Run: composer require symfony/finder');
@@ -223,76 +226,65 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
223226
try {
224227
$instance = $attribute->newInstance();
225228

226-
switch ($attributeClassName) {
227-
case McpTool::class:
228-
$name = ElementMetadataResolver::resolveName($method, $instance->name);
229-
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
230-
$inputSchema = $this->schemaGenerator->generate($method);
231-
$outputSchema = $this->schemaGenerator->generateOutputSchema($method);
232-
$tool = new Tool(
233-
name: $name,
234-
title: $instance->title,
235-
inputSchema: $inputSchema,
236-
description: $description,
237-
annotations: $instance->annotations,
238-
icons: $instance->icons,
239-
meta: $instance->meta,
240-
outputSchema: $outputSchema,
241-
);
242-
$tools[$name] = new ToolReference($tool, [$className, $methodName]);
243-
++$discoveredCount['tools'];
244-
break;
245-
246-
case McpResource::class:
247-
$name = ElementMetadataResolver::resolveName($method, $instance->name);
248-
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
249-
$resource = new ResourceDefinition(
250-
$instance->uri,
251-
$name,
252-
$instance->title,
253-
$description,
254-
$instance->mimeType,
255-
$instance->annotations,
256-
$instance->size,
257-
$instance->icons,
258-
$instance->meta,
259-
);
260-
$resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName]);
261-
262-
++$discoveredCount['resources'];
263-
break;
264-
265-
case McpPrompt::class:
266-
$docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null);
267-
$name = ElementMetadataResolver::resolveName($method, $instance->name);
268-
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
269-
$arguments = [];
270-
$paramTags = $this->docBlockParser->getParamTags($docBlock);
271-
foreach ($method->getParameters() as $param) {
272-
$reflectionType = $param->getType();
273-
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
274-
continue;
275-
}
276-
$paramTag = $paramTags['$'.$param->getName()] ?? null;
277-
$arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable());
229+
if ($instance instanceof McpTool) {
230+
$name = ElementMetadataResolver::resolveName($method, $instance->name);
231+
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
232+
$inputSchema = $this->schemaGenerator->generate($method);
233+
$outputSchema = $this->schemaGenerator->generateOutputSchema($method);
234+
$tool = new Tool(
235+
name: $name,
236+
title: $instance->title,
237+
inputSchema: $inputSchema,
238+
description: $description,
239+
annotations: $instance->annotations,
240+
icons: $instance->icons,
241+
meta: $instance->meta,
242+
outputSchema: $outputSchema,
243+
);
244+
$tools[$name] = new ToolReference($tool, [$className, $methodName]);
245+
++$discoveredCount['tools'];
246+
} elseif ($instance instanceof McpResource) {
247+
$name = ElementMetadataResolver::resolveName($method, $instance->name);
248+
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
249+
$resource = new ResourceDefinition(
250+
$instance->uri,
251+
$name,
252+
$instance->title,
253+
$description,
254+
$instance->mimeType,
255+
$instance->annotations,
256+
$instance->size,
257+
$instance->icons,
258+
$instance->meta,
259+
);
260+
$resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName]);
261+
262+
++$discoveredCount['resources'];
263+
} elseif ($instance instanceof McpPrompt) {
264+
$docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null);
265+
$name = ElementMetadataResolver::resolveName($method, $instance->name);
266+
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
267+
$arguments = [];
268+
$paramTags = $this->docBlockParser->getParamTags($docBlock);
269+
foreach ($method->getParameters() as $param) {
270+
$reflectionType = $param->getType();
271+
if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
272+
continue;
278273
}
279-
$prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
280-
$completionProviders = $this->getCompletionProviders($method);
281-
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], $completionProviders);
282-
++$discoveredCount['prompts'];
283-
break;
284-
285-
case McpResourceTemplate::class:
286-
$name = ElementMetadataResolver::resolveName($method, $instance->name);
287-
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
288-
$mimeType = $instance->mimeType;
289-
$annotations = $instance->annotations;
290-
$meta = $instance->meta ?? null;
291-
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $instance->title, $description, $mimeType, $annotations, $meta);
292-
$completionProviders = $this->getCompletionProviders($method);
293-
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
294-
++$discoveredCount['resourceTemplates'];
295-
break;
274+
$paramTag = $paramTags['$'.$param->getName()] ?? null;
275+
$arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable());
276+
}
277+
$prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
278+
$completionProviders = $this->getCompletionProviders($method);
279+
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], $completionProviders);
280+
++$discoveredCount['prompts'];
281+
} elseif ($instance instanceof McpResourceTemplate) {
282+
$name = ElementMetadataResolver::resolveName($method, $instance->name);
283+
$description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
284+
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $instance->title, $description, $instance->mimeType, $instance->annotations, $instance->meta);
285+
$completionProviders = $this->getCompletionProviders($method);
286+
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
287+
++$discoveredCount['resourceTemplates'];
296288
}
297289
} catch (ExceptionInterface $e) {
298290
$this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", [
@@ -308,7 +300,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
308300
}
309301

310302
/**
311-
* @return array<string, string|ProviderInterface>
303+
* @return array<string, class-string<ProviderInterface>|ProviderInterface>
312304
*/
313305
private function getCompletionProviders(\ReflectionMethod $reflectionMethod): array
314306
{
@@ -431,17 +423,13 @@ private function getClassFromFile(SplFileInfo $file): ?string
431423
}
432424

433425
foreach ($potentialClasses as $potentialClass) {
434-
if (class_exists($potentialClass, true)) {
426+
if (class_exists($potentialClass, true) || interface_exists($potentialClass, true) || trait_exists($potentialClass, true)) {
435427
return $potentialClass;
436428
}
437429
}
438430

439431
if (!empty($potentialClasses)) {
440-
if (!class_exists($potentialClasses[0], false)) {
441-
$this->logger->debug('getClassFromFile returning potential non-class type. Are you sure this class has been autoloaded?', ['file' => $file->getPathname(), 'type' => $potentialClasses[0]]);
442-
}
443-
444-
return $potentialClasses[0];
432+
$this->logger->debug('getClassFromFile found no loadable type. Are you sure this class has been autoloaded?', ['file' => $file->getPathname(), 'type' => $potentialClasses[0]]);
445433
}
446434

447435
return null;

src/Capability/Discovery/SchemaGenerator.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@
4949
* enum?: array<int, int|float|string|null>,
5050
* items?: array<string, mixed>,
5151
* }
52-
* @phpstan-type VariadicParameterSchema array{
53-
* type: 'array',
54-
* items?: array<string, mixed>,
55-
* description?: string,
56-
* parameter_schema?: array<string, mixed>
57-
* }
5852
*
5953
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
6054
*/
@@ -328,7 +322,7 @@ private function buildInferredParameterSchema(array $paramInfo): array
328322
*
329323
* @param ParameterInfo $paramInfo
330324
*
331-
* @return VariadicParameterSchema
325+
* @return array<string, mixed>
332326
*/
333327
private function buildVariadicParameterSchema(array $paramInfo): array
334328
{
@@ -536,7 +530,8 @@ private function parseParametersInfo(\ReflectionMethod|\ReflectionFunction $refl
536530

537531
$paramName = $rp->getName();
538532
if (\in_array(strtolower($paramName), ['_session', '_request'], true)) {
539-
throw new InvalidArgumentException(\sprintf('Handler method "%s::%s" has parameter named "%s" which is not allowed. Please change the name of that parameter.', $reflection->class, $reflection->name, $paramName));
533+
$handlerName = $reflection instanceof \ReflectionMethod ? $reflection->class.'::'.$reflection->name : $reflection->name;
534+
throw new InvalidArgumentException(\sprintf('Handler "%s" has parameter named "%s" which is not allowed. Please change the name of that parameter.', $handlerName, $paramName));
540535
}
541536
$paramTag = $paramTags['$'.$paramName] ?? null;
542537

@@ -693,8 +688,10 @@ private function getTypeStringFromReflection(?\ReflectionType $type, bool $nativ
693688
// Remove leading backslash from class names, but handle built-ins like 'int' or unions like 'int|string'
694689
if (str_contains($typeString, '\\')) {
695690
$parts = preg_split('/([|&])/', $typeString, -1, \PREG_SPLIT_DELIM_CAPTURE);
696-
$processedParts = array_map(static fn ($part) => str_starts_with($part, '\\') ? ltrim($part, '\\') : $part, $parts);
697-
$typeString = implode('', $processedParts);
691+
if (false !== $parts) {
692+
$processedParts = array_map(static fn ($part) => str_starts_with($part, '\\') ? ltrim($part, '\\') : $part, $parts);
693+
$typeString = implode('', $processedParts);
694+
}
698695
}
699696

700697
return $typeString ?: 'mixed';

0 commit comments

Comments
 (0)