diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 6c82457..35e4fa3 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -62,56 +62,50 @@ 'path' => __DIR__ . '/src/Interfaces/TracingProcessorInterface.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'description\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', + 'message' => '#^Call to function assert\\(\\) with false will always evaluate to false\\.$#', + 'identifier' => 'function.impossibleType', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'enum\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', + 'message' => '#^Call to function assert\\(\\) with true will always evaluate to true\\.$#', + 'identifier' => 'function.alreadyNarrowedType', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'type\' on mixed\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', + 'message' => '#^Call to function is_array\\(\\) with string will always evaluate to false\\.$#', + 'identifier' => 'function.impossibleType', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Offset \'properties\' on array\\{properties\\: array\\{type\\: string, 0\\: mixed\\}, required\\: array\\, type\\: string\\} in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.offset', - 'count' => 1, - 'path' => __DIR__ . '/src/Mcp/McpTool.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Offset \'required\' on array\\{properties\\: array\\{type\\: string, 0\\: mixed\\}, required\\: array\\, type\\: string\\} in isset\\(\\) always exists and is not nullable\\.$#', - 'identifier' => 'isset.offset', + 'message' => '#^Offset \'description\' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#', + 'identifier' => 'nullCoalesce.offset', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$name of method Swis\\\\Agents\\\\DynamicTool\\:\\:withDynamicProperty\\(\\) expects string, int\\|string given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Offset \'enum\' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#', + 'identifier' => 'nullCoalesce.offset', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$type of method Swis\\\\Agents\\\\DynamicTool\\:\\:withDynamicProperty\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Offset \'properties\' on array\\{properties\\: array\\{type\\: string, 0\\: mixed\\}, required\\: array\\, type\\: string\\} in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.offset', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#3 \\$description of method Swis\\\\Agents\\\\DynamicTool\\:\\:withDynamicProperty\\(\\) expects string, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Offset \'required\' on array\\{properties\\: array\\{type\\: string, 0\\: mixed\\}, required\\: array\\, type\\: string\\} in isset\\(\\) always exists and is not nullable\\.$#', + 'identifier' => 'isset.offset', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; $ignoreErrors[] = [ - 'message' => '#^Parameter \\#5 \\$enum of method Swis\\\\Agents\\\\DynamicTool\\:\\:withDynamicProperty\\(\\) expects array\\\\|null, mixed given\\.$#', - 'identifier' => 'argument.type', + 'message' => '#^Offset \'type\' on \\*NEVER\\* on left side of \\?\\? always exists and is not nullable\\.$#', + 'identifier' => 'nullCoalesce.offset', 'count' => 1, 'path' => __DIR__ . '/src/Mcp/McpTool.php', ]; diff --git a/src/DynamicTool.php b/src/DynamicTool.php index 7207a69..4dfb000 100644 --- a/src/DynamicTool.php +++ b/src/DynamicTool.php @@ -10,7 +10,7 @@ * Tools are executable components that provide specific functionality to agents. * They can be invoked by agents to perform operations and return results. * - * @phpstan-type ToolProperty array{type: string, description: string, required: bool, enum?: array, itemsType?: string, objectClass?: string} + * @phpstan-type ToolProperty array{type: string, description: string, required: bool, enum?: array, itemsType?: string, objectClass?: string, schema?: array} */ abstract class DynamicTool extends Tool { @@ -56,6 +56,7 @@ public function description(): ?string * @param array|null $enum Optional enum values * @param string|null $itemsType For array properties, the type of items in the array * @param string|null $objectClass For object properties, the class to cast to/from + * @param array|null $customSchema Use a custom schema definition * @return self */ public function withDynamicProperty( @@ -65,7 +66,8 @@ public function withDynamicProperty( bool $required = false, ?array $enum = null, ?string $itemsType = null, - ?string $objectClass = null + ?string $objectClass = null, + ?array $customSchema = null ): self { $property = [ 'type' => $type, @@ -85,6 +87,10 @@ public function withDynamicProperty( $property['objectClass'] = $objectClass; } + if ($customSchema !== null) { + $property['schema'] = $customSchema; + } + $this->dynamicProperties[$name] = $property; return $this; diff --git a/src/Helpers/ToolHelper.php b/src/Helpers/ToolHelper.php index c16f451..cbfaf8d 100644 --- a/src/Helpers/ToolHelper.php +++ b/src/Helpers/ToolHelper.php @@ -261,6 +261,14 @@ private static function processDynamicProperties(DynamicTool $tool, array &$prop { $dynamicProps = $tool->getDynamicProperties(); foreach ($dynamicProps as $propName => $propDetails) { + + // Use the schema definition when available + if (isset($propDetails['schema'])) { + $properties[$propName] = $propDetails['schema']; + + continue; + } + $properties[$propName] = [ 'type' => $propDetails['type'], 'description' => $propDetails['description'], diff --git a/src/Mcp/McpTool.php b/src/Mcp/McpTool.php index c5eeea6..eb68c75 100644 --- a/src/Mcp/McpTool.php +++ b/src/Mcp/McpTool.php @@ -78,9 +78,13 @@ protected function registerDynamicProperties(): void if (empty($schema) || ! isset($schema['properties'])) { return; } + assert(is_array($schema['properties'])); // For each property in the schema, register a dynamic property foreach ($schema['properties'] as $propName => $propDetails) { + assert(is_string($propName)); + assert(is_array($propDetails)); + // Get the property description $description = $propDetails['description'] ?? "Parameter: {$propName}"; @@ -96,8 +100,23 @@ protected function registerDynamicProperties(): void // Get enum values if they exist $enum = $propDetails['enum'] ?? null; + // Get the items type when the type is array + $itemsType = $propDetails['items']['type'] ?? null; + + // Cast object types to stdClass + $objectClass = $itemsType === 'object' ? \stdClass::class : null; + // Register the dynamic property - $this->withDynamicProperty($propName, $type, $description, $required, $enum); + $this->withDynamicProperty( + name: $propName, + type: $type, + description: $description, + required: $required, + enum: $enum, + itemsType: $itemsType, + objectClass: $objectClass, + customSchema: $propDetails, + ); } } }