typeValidator = new ParameterTypeValidator(); } public function getAttributeClass(): string { return McpTool::class; } public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): ?array { if (! $reflectionTarget instanceof WrappedReflectionMethod || ! $attributeInstance instanceof McpTool) { return null; } $class = $reflectionTarget->getDeclaringClass(); // Check if method has multiple attributes (multi-purpose) $hasMultipleAttributes = $this->hasMultiplePurposeAttributes($reflectionTarget); // If multi-purpose, validate that all parameters are builtin types if ($hasMultipleAttributes) { $parameters = $reflectionTarget->getParameters()->toArray(); $reflectionParameters = []; foreach ($parameters as $param) { $reflectionParameters[] = $param->getType(); } if (! $this->typeValidator->hasOnlyBuiltinParameters($reflectionParameters)) { // Skip this attribute if parameters are not all builtin return null; } } // Get other attributes for metadata $otherAttributes = $this->getOtherPurposeAttributes($reflectionTarget); return [ 'name' => $attributeInstance->name, 'description' => $attributeInstance->description, 'inputSchema' => $this->generateInputSchema($reflectionTarget, $attributeInstance), 'class' => $class->getFullyQualified(), 'method' => $reflectionTarget->getName(), 'parameters' => $this->extractParameters($reflectionTarget), 'multi_purpose' => $hasMultipleAttributes, 'other_attributes' => $otherAttributes, ]; } /** * Check if method has multiple purpose attributes (McpTool, ConsoleCommand, Route) */ private function hasMultiplePurposeAttributes(WrappedReflectionMethod $method): bool { $attributes = $method->getAttributes(); $purposeAttributeCount = 0; foreach ($attributes as $attribute) { $attributeName = $attribute->getName(); if (in_array($attributeName, [ McpTool::class, ConsoleCommand::class, Route::class, ], true)) { $purposeAttributeCount++; } } return $purposeAttributeCount > 1; } /** * Get other purpose attributes on the same method * * @return array */ private function getOtherPurposeAttributes(WrappedReflectionMethod $method): array { $attributes = $method->getAttributes(); $otherAttributes = []; foreach ($attributes as $attribute) { $attributeName = $attribute->getName(); if (in_array($attributeName, [ ConsoleCommand::class, Route::class, ], true)) { $otherAttributes[] = $attributeName; } } return $otherAttributes; } private function generateInputSchema(WrappedReflectionMethod $method, McpTool $tool): array { $schema = [ 'type' => 'object', 'properties' => [], 'required' => [], 'title' => $tool->name, 'description' => $tool->description, ]; if (! empty($tool->category)) { $schema['category'] = $tool->category; } if (! empty($tool->tags)) { $schema['tags'] = $tool->tags; } foreach ($method->getParameters() as $param) { $paramName = $param->getName(); $paramSchema = $this->generateParameterSchema($param); $schema['properties'][$paramName] = $paramSchema; if (! $param->isOptional()) { $schema['required'][] = $paramName; } } return $schema; } private function generateParameterSchema(\ReflectionParameter $param): array { $type = $param->getType(); $schema = $this->mapPhpTypeToJsonSchema($type); // Add description based on parameter name $schema['description'] = $this->generateParameterDescription($param); // Add default value if parameter is optional if ($param->isOptional()) { try { $defaultValue = $param->getDefaultValue(); $schema['default'] = $defaultValue; } catch (\ReflectionException) { // Some defaults (like class constants) can't be retrieved $schema['default'] = null; } } return $schema; } private function mapPhpTypeToJsonSchema(?\ReflectionType $type): array { if ($type === null) { return ['type' => 'string', 'description' => 'Mixed type parameter']; } if ($type instanceof ReflectionNamedType) { return $this->mapNamedTypeToSchema($type); } if ($type instanceof ReflectionUnionType) { return $this->mapUnionTypeToSchema($type); } if ($type instanceof ReflectionIntersectionType) { return ['type' => 'object', 'description' => 'Intersection type']; } return ['type' => 'string', 'description' => 'Unknown type']; } private function mapNamedTypeToSchema(ReflectionNamedType $type): array { $typeName = $type->getName(); return match ($typeName) { 'string' => ['type' => 'string'], 'int' => ['type' => 'integer'], 'float' => ['type' => 'number'], 'bool' => ['type' => 'boolean'], 'array' => ['type' => 'array', 'items' => ['type' => 'string']], 'null' => ['type' => 'null'], 'mixed' => ['description' => 'Mixed type - can be any value'], default => $this->mapComplexTypeToSchema($typeName) }; } private function mapComplexTypeToSchema(string $typeName): array { // Handle enums if (class_exists($typeName) && enum_exists($typeName)) { return $this->mapEnumToSchema($typeName); } // Handle OutputFormat specifically if ($typeName === OutputFormat::class || str_ends_with($typeName, 'OutputFormat')) { return [ 'type' => 'string', 'enum' => ['array', 'json', 'table', 'tree', 'text', 'mermaid', 'plantuml'], 'default' => 'array', 'description' => 'Output format for the response', ]; } // Handle other classes if (class_exists($typeName)) { return [ 'type' => 'object', 'description' => "Instance of {$typeName}", ]; } // Fallback for unknown types return [ 'type' => 'string', 'description' => "Parameter of type {$typeName}", ]; } private function mapEnumToSchema(string $enumClass): array { try { $reflection = new ReflectionEnum($enumClass); $cases = []; foreach ($reflection->getCases() as $case) { $cases[] = $case->getValue(); } return [ 'type' => 'string', 'enum' => $cases, 'description' => "Enum values from {$enumClass}", ]; } catch (\ReflectionException) { return [ 'type' => 'string', 'description' => "Enum type {$enumClass}", ]; } } private function mapUnionTypeToSchema(ReflectionUnionType $type): array { $types = []; $hasNull = false; foreach ($type->getTypes() as $unionType) { if ($unionType instanceof ReflectionNamedType && $unionType->getName() === 'null') { $hasNull = true; continue; } $schema = $this->mapPhpTypeToJsonSchema($unionType); $types[] = $schema; } if (count($types) === 1) { $schema = $types[0]; if ($hasNull) { $schema['nullable'] = true; } return $schema; } return [ 'anyOf' => $types, 'nullable' => $hasNull, 'description' => 'Union type parameter', ]; } private function generateParameterDescription(\ReflectionParameter $param): string { $name = $param->getName(); $type = $param->getType(); // Generate human-readable descriptions based on parameter names $descriptions = [ 'path' => 'File or directory path', 'file' => 'File path', 'directory' => 'Directory path', 'format' => 'Output format for the response', 'includeHidden' => 'Whether to include hidden files', 'includeAnalysis' => 'Whether to include detailed analysis', 'includeSecurity' => 'Whether to include security assessment', 'includeMetrics' => 'Whether to include performance metrics', 'includeHealthCheck' => 'Whether to include health check information', 'controller' => 'Controller class name to filter by', 'method' => 'HTTP method to filter by', 'focus' => 'Area of focus for analysis', 'task' => 'Task description for the agent', 'sortBy' => 'Field to sort results by', 'sortOrder' => 'Sort order (asc or desc)', 'pattern' => 'Search pattern or regex', 'limit' => 'Maximum number of results to return', 'offset' => 'Number of results to skip', 'recursive' => 'Whether to search recursively', 'caseSensitive' => 'Whether search should be case sensitive', ]; if (isset($descriptions[$name])) { return $descriptions[$name]; } $typeName = $type instanceof ReflectionNamedType ? $type->getName() : 'mixed'; return "Parameter of type {$typeName}"; } private function extractParameters(WrappedReflectionMethod $method): array { $parameters = []; foreach ($method->getParameters() as $param) { $type = $param->getType(); $parameters[] = [ 'name' => $param->getName(), 'type' => $type ? ($type instanceof \ReflectionNamedType ? $type->getName() : 'mixed') : 'mixed', 'required' => ! $param->isOptional(), 'default' => $param->isOptional() ? $param->getDefaultValue() : null, ]; } return $parameters; } }