Skip to main content
Glama

CTX: Context as Code (CaC) tool

by context-hub
MIT License
235
  • Apple
  • Linux
creating-mcp-tools.md10.1 kB
# Creating New MCP Tools in CTX This guide explains how to create new MCP (Model Context Protocol) tools in the CTX context generator. ## Overview CTX uses a modern attribute-based approach for defining MCP tools with: - **Typed DTOs** for input validation using Spiral's JSON Schema Generator - **Automatic schema generation** from PHP types and attributes - **Clean separation** between input validation and business logic ## Step-by-Step Guide ### 1. Create the Input DTO Class Create a DTO (Data Transfer Object) class to define your tool's input parameters: ```php <?php declare(strict_types=1); namespace Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory\Dto; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Enum; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Range; final readonly class YourToolRequest { public function __construct( #[Field( description: 'Description of this required parameter', )] public string $requiredParam, #[Field( description: 'Description of optional parameter with default', default: 'default-value', )] public string $optionalParam = 'default-value', #[Field( description: 'Numeric parameter with range validation', default: 10, )] #[Range(min: 1, max: 100)] public int $numericParam = 10, #[Field( description: 'Enum parameter with allowed values', default: 'option1', )] #[Enum(values: ['option1', 'option2', 'option3'])] public string $enumParam = 'option1', #[Field( description: 'Optional nullable parameter', )] public ?string $nullableParam = null, ) {} } ``` ### 2. Create the Tool Action Class Create the main tool action class: ```php <?php declare(strict_types=1); namespace Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory;`` use Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory\Dto\YourToolRequest; use Butschster\ContextGenerator\McpServer\Attribute\InputSchema; use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; use Mcp\Types\CallToolResult; use Mcp\Types\TextContent; use Psr\Log\LoggerInterface; #[Tool( name: 'your-tool-name', description: 'Clear description of what your tool does and when to use it', title: 'Human-Readable Tool Title', )] #[InputSchema(class: YourToolRequest::class)] final readonly class YourToolAction { public function __construct( private LoggerInterface $logger, // Inject other dependencies you need ) {} #[Post(path: '/tools/call/your-tool-name', name: 'tools.your-tool-name')] public function __invoke(YourToolRequest $request): CallToolResult { $this->logger->info('Processing your-tool-name tool'); try { // Access typed parameters directly from the request DTO $result = $this->performYourLogic( requiredParam: $request->requiredParam, optionalParam: $request->optionalParam, numericParam: $request->numericParam, enumParam: $request->enumParam, nullableParam: $request->nullableParam, ); return new CallToolResult([ new TextContent( text: $result, ), ]); } catch (\Throwable $e) { $this->logger->error('Error in your-tool-name tool', [ 'error' => $e->getMessage(), ]); return new CallToolResult([ new TextContent( text: 'Error: ' . $e->getMessage(), ), ], isError: true); } } private function performYourLogic( string $requiredParam, string $optionalParam, int $numericParam, string $enumParam, ?string $nullableParam, ): string { // Your tool's business logic here return 'Tool result'; } } ``` ## Available Spiral JSON Schema Attributes ### Core Attributes - **`#[Field]`** - Basic field definition with description, title, default value - **`#[AdditionalProperties]`** - Control additional properties on objects ### Validation Constraints - **`#[Enum]`** - Restrict to specific values: `#[Enum(values: ['a', 'b', 'c'])]` - **`#[Range]`** - Numeric ranges: `#[Range(min: 0, max: 100)]` - **`#[Length]`** - String length: `#[Length(min: 1, max: 255)]` - **`#[Pattern]`** - Regex validation: `#[Pattern(pattern: '^[a-z]+$')]` - **`#[MultipleOf]`** - Numeric multiple: `#[MultipleOf(value: 5)]` - **`#[Items]`** - Array item constraints ### Field Attribute Options ```php #[Field( title: 'Human readable title', description: 'Detailed description for AI context', default: 'default-value', format: Format::Email, // Built-in formats )] ``` ## Directory Structure Organize your tools following the existing pattern: ``` src/McpServer/Action/Tools/ ├── YourCategory/ │ ├── Dto/ │ │ ├── YourToolRequest.php │ │ └── AnotherToolRequest.php │ ├── YourToolAction.php │ └── AnotherToolAction.php ``` ## Complex Input Types ### Nested Objects For complex nested parameters, create separate DTO classes: ```php // Nested DTO final readonly class NestedConfig { public function __construct( #[Field(description: 'Enable feature')] public bool $enabled = false, #[Field(description: 'Configuration value')] public string $value = '', ) {} } // Main DTO using nested object final readonly class ComplexToolRequest { public function __construct( #[Field(description: 'Basic parameter')] public string $basic, #[Field(description: 'Complex nested configuration')] public ?NestedConfig $config = null, ) {} } ``` ### Arrays Handle array inputs with proper typing: ```php final readonly class ArrayToolRequest { public function __construct( #[Field(description: 'List of strings')] /** @var string[] */ public array $stringList = [], #[Field(description: 'List of integers')] /** @var int[] */ public array $numbers = [], ) {} } ``` ## Best Practices ### 1. Naming Conventions - **Tool names**: Use kebab-case (`my-tool-name`) - **DTO classes**: Use PascalCase with `Request` suffix (`MyToolRequest`) - **Action classes**: Use PascalCase with `Action` suffix (`MyToolAction`) ### 2. Error Handling Always wrap your logic in try-catch blocks and return proper error responses: ```php try { // Your logic return new CallToolResult([new TextContent(text: $result)]); } catch (\Throwable $e) { $this->logger->error('Tool error', ['error' => $e->getMessage()]); return new CallToolResult([ new TextContent(text: 'Error: ' . $e->getMessage()) ], isError: true); } ``` ### 3. Logging Use the injected logger for debugging and monitoring: ```php $this->logger->info('Processing tool', ['param' => $request->param]); $this->logger->error('Tool failed', ['error' => $e->getMessage()]); ``` ### 4. Input Validation - Use readonly DTOs for immutability - Leverage PHP's type system and Spiral's validation attributes - Provide clear descriptions for AI context understanding - Set sensible defaults where appropriate ### 5. Route Naming Follow the established pattern for route naming: - Path: `/tools/call/{tool-name}` - Name: `tools.{category}.{action}` or `tools.{tool-name}` ## Tool Registration While CTX uses attribute-based tool discovery, tools must be registered in the MCP server bootloader to be available. Follow these steps: ### 1. Register Your Tool Classes Add your tool action classes to `src/McpServer/McpServerBootloader.php`: ```php // 1. Import your tool actions use YourNamespace\Action\Tools\YourCategory\YourToolAction; // 2. Add to the actions() method if ($config->isYourCategoryEnabled()) { $actions = [ ...$actions, YourToolAction::class, ]; } ``` ### 2. Add Configuration Support (Optional) For configurable tools, add settings to `McpConfig.php`: ```php // In the $config array 'your_category' => [ 'enable' => true, 'your_tool' => true, ], // Add configuration methods public function isYourCategoryEnabled(): bool { return $this->config['your_category']['enable'] ?? true; } ``` ### 3. Environment Variables (Optional) Add environment variable support in `McpServerBootloader::init()`: ```php 'your_category' => [ 'enable' => (bool) $env->get('MCP_YOUR_CATEGORY', true), 'your_tool' => (bool) $env->get('MCP_YOUR_TOOL', true), ], ``` ### Registration Examples #### Simple Tool Registration For basic tools without configuration: ```php // In actions() method $actions = [ ...$actions, YourSimpleToolAction::class, ]; ``` #### Configurable Tool Category For tool categories with multiple tools: ```php if ($config->isGitOperationsEnabled()) { $gitActions = []; if ($config->isGitStatusEnabled()) { $gitActions[] = GitStatusAction::class; } if ($config->isGitCommitEnabled()) { $gitActions[] = GitCommitAction::class; } $actions = [...$actions, ...$gitActions]; } ``` ### Tool Discovery After registration, CTX discovers tools through attribute scanning of the registered classes. The `#[Tool]` and `#[InputSchema]` attributes define the tool metadata and input schema automatically. ## Testing Your Tool 1. **Start the MCP server**: `php ctx server` 2. **List tools**: Use the `tools/list` endpoint to verify your tool appears 3. **Test execution**: Call your tool through the MCP protocol 4. **Check logs**: Monitor application logs for any issues This modern approach provides type safety, automatic validation, and excellent IDE support while maintaining clean separation of concerns.

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/context-hub/generator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server