Skip to content

Commit a8b34a2

Browse files
authored
Merge pull request #231 from context-hub/feature/tool-json-schema
Implement JSON schema geenerator for tools
2 parents e67153c + 97c7470 commit a8b34a2

Some content is hidden

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

53 files changed

+2855
-861
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"php": "^8.3",
2020
"ext-curl": "*",
2121
"guzzlehttp/guzzle": "^7.0",
22+
"cuyz/valinor": "^1.7",
2223
"league/html-to-markdown": "^5.1",
2324
"psr-discovery/http-client-implementations": "^1.0",
2425
"psr-discovery/http-factory-implementations": "^1.0",
@@ -37,6 +38,7 @@
3738
"spiral/exceptions": "^3.15",
3839
"spiral/boot": "^3.15",
3940
"spiral/files": "^3.15",
41+
"spiral/json-schema-generator": "^2.1",
4042
"logiscape/mcp-sdk-php": "^1.0",
4143
"league/route": "^6.2",
4244
"laminas/laminas-diactoros": "^3.5",

docs/creating-mcp-tools.md

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
# Creating New MCP Tools in CTX
2+
3+
This guide explains how to create new MCP (Model Context Protocol) tools in the CTX context generator.
4+
5+
## Overview
6+
7+
CTX uses a modern attribute-based approach for defining MCP tools with:
8+
9+
- **Typed DTOs** for input validation using Spiral's JSON Schema Generator
10+
- **Automatic schema generation** from PHP types and attributes
11+
- **Clean separation** between input validation and business logic
12+
13+
## Step-by-Step Guide
14+
15+
### 1. Create the Input DTO Class
16+
17+
Create a DTO (Data Transfer Object) class to define your tool's input parameters:
18+
19+
```php
20+
<?php
21+
22+
declare(strict_types=1);
23+
24+
namespace Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory\Dto;
25+
26+
use Spiral\JsonSchemaGenerator\Attribute\Field;
27+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Enum;
28+
use Spiral\JsonSchemaGenerator\Attribute\Constraint\Range;
29+
30+
final readonly class YourToolRequest
31+
{
32+
public function __construct(
33+
#[Field(
34+
description: 'Description of this required parameter',
35+
)]
36+
public string $requiredParam,
37+
38+
#[Field(
39+
description: 'Description of optional parameter with default',
40+
default: 'default-value',
41+
)]
42+
public string $optionalParam = 'default-value',
43+
44+
#[Field(
45+
description: 'Numeric parameter with range validation',
46+
default: 10,
47+
)]
48+
#[Range(min: 1, max: 100)]
49+
public int $numericParam = 10,
50+
51+
#[Field(
52+
description: 'Enum parameter with allowed values',
53+
default: 'option1',
54+
)]
55+
#[Enum(values: ['option1', 'option2', 'option3'])]
56+
public string $enumParam = 'option1',
57+
58+
#[Field(
59+
description: 'Optional nullable parameter',
60+
)]
61+
public ?string $nullableParam = null,
62+
) {}
63+
}
64+
```
65+
66+
### 2. Create the Tool Action Class
67+
68+
Create the main tool action class:
69+
70+
```php
71+
<?php
72+
73+
declare(strict_types=1);
74+
75+
namespace Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory;``
76+
77+
use Butschster\ContextGenerator\McpServer\Action\Tools\YourCategory\Dto\YourToolRequest;
78+
use Butschster\ContextGenerator\McpServer\Attribute\InputSchema;
79+
use Butschster\ContextGenerator\McpServer\Attribute\Tool;
80+
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post;
81+
use Mcp\Types\CallToolResult;
82+
use Mcp\Types\TextContent;
83+
use Psr\Log\LoggerInterface;
84+
85+
#[Tool(
86+
name: 'your-tool-name',
87+
description: 'Clear description of what your tool does and when to use it',
88+
title: 'Human-Readable Tool Title',
89+
)]
90+
#[InputSchema(class: YourToolRequest::class)]
91+
final readonly class YourToolAction
92+
{
93+
public function __construct(
94+
private LoggerInterface $logger,
95+
// Inject other dependencies you need
96+
) {}
97+
98+
#[Post(path: '/tools/call/your-tool-name', name: 'tools.your-tool-name')]
99+
public function __invoke(YourToolRequest $request): CallToolResult
100+
{
101+
$this->logger->info('Processing your-tool-name tool');
102+
103+
try {
104+
// Access typed parameters directly from the request DTO
105+
$result = $this->performYourLogic(
106+
requiredParam: $request->requiredParam,
107+
optionalParam: $request->optionalParam,
108+
numericParam: $request->numericParam,
109+
enumParam: $request->enumParam,
110+
nullableParam: $request->nullableParam,
111+
);
112+
113+
return new CallToolResult([
114+
new TextContent(
115+
text: $result,
116+
),
117+
]);
118+
} catch (\Throwable $e) {
119+
$this->logger->error('Error in your-tool-name tool', [
120+
'error' => $e->getMessage(),
121+
]);
122+
123+
return new CallToolResult([
124+
new TextContent(
125+
text: 'Error: ' . $e->getMessage(),
126+
),
127+
], isError: true);
128+
}
129+
}
130+
131+
private function performYourLogic(
132+
string $requiredParam,
133+
string $optionalParam,
134+
int $numericParam,
135+
string $enumParam,
136+
?string $nullableParam,
137+
): string {
138+
// Your tool's business logic here
139+
return 'Tool result';
140+
}
141+
}
142+
```
143+
144+
## Available Spiral JSON Schema Attributes
145+
146+
### Core Attributes
147+
148+
- **`#[Field]`** - Basic field definition with description, title, default value
149+
- **`#[AdditionalProperties]`** - Control additional properties on objects
150+
151+
### Validation Constraints
152+
153+
- **`#[Enum]`** - Restrict to specific values: `#[Enum(values: ['a', 'b', 'c'])]`
154+
- **`#[Range]`** - Numeric ranges: `#[Range(min: 0, max: 100)]`
155+
- **`#[Length]`** - String length: `#[Length(min: 1, max: 255)]`
156+
- **`#[Pattern]`** - Regex validation: `#[Pattern(pattern: '^[a-z]+$')]`
157+
- **`#[MultipleOf]`** - Numeric multiple: `#[MultipleOf(value: 5)]`
158+
- **`#[Items]`** - Array item constraints
159+
160+
### Field Attribute Options
161+
162+
```php
163+
#[Field(
164+
title: 'Human readable title',
165+
description: 'Detailed description for AI context',
166+
default: 'default-value',
167+
format: Format::Email, // Built-in formats
168+
)]
169+
```
170+
171+
## Directory Structure
172+
173+
Organize your tools following the existing pattern:
174+
175+
```
176+
src/McpServer/Action/Tools/
177+
├── YourCategory/
178+
│ ├── Dto/
179+
│ │ ├── YourToolRequest.php
180+
│ │ └── AnotherToolRequest.php
181+
│ ├── YourToolAction.php
182+
│ └── AnotherToolAction.php
183+
```
184+
185+
## Complex Input Types
186+
187+
### Nested Objects
188+
189+
For complex nested parameters, create separate DTO classes:
190+
191+
```php
192+
// Nested DTO
193+
final readonly class NestedConfig
194+
{
195+
public function __construct(
196+
#[Field(description: 'Enable feature')]
197+
public bool $enabled = false,
198+
199+
#[Field(description: 'Configuration value')]
200+
public string $value = '',
201+
) {}
202+
}
203+
204+
// Main DTO using nested object
205+
final readonly class ComplexToolRequest
206+
{
207+
public function __construct(
208+
#[Field(description: 'Basic parameter')]
209+
public string $basic,
210+
211+
#[Field(description: 'Complex nested configuration')]
212+
public ?NestedConfig $config = null,
213+
) {}
214+
}
215+
```
216+
217+
### Arrays
218+
219+
Handle array inputs with proper typing:
220+
221+
```php
222+
final readonly class ArrayToolRequest
223+
{
224+
public function __construct(
225+
#[Field(description: 'List of strings')]
226+
/** @var string[] */
227+
public array $stringList = [],
228+
229+
#[Field(description: 'List of integers')]
230+
/** @var int[] */
231+
public array $numbers = [],
232+
) {}
233+
}
234+
```
235+
236+
## Best Practices
237+
238+
### 1. Naming Conventions
239+
240+
- **Tool names**: Use kebab-case (`my-tool-name`)
241+
- **DTO classes**: Use PascalCase with `Request` suffix (`MyToolRequest`)
242+
- **Action classes**: Use PascalCase with `Action` suffix (`MyToolAction`)
243+
244+
### 2. Error Handling
245+
246+
Always wrap your logic in try-catch blocks and return proper error responses:
247+
248+
```php
249+
try {
250+
// Your logic
251+
return new CallToolResult([new TextContent(text: $result)]);
252+
} catch (\Throwable $e) {
253+
$this->logger->error('Tool error', ['error' => $e->getMessage()]);
254+
return new CallToolResult([
255+
new TextContent(text: 'Error: ' . $e->getMessage())
256+
], isError: true);
257+
}
258+
```
259+
260+
### 3. Logging
261+
262+
Use the injected logger for debugging and monitoring:
263+
264+
```php
265+
$this->logger->info('Processing tool', ['param' => $request->param]);
266+
$this->logger->error('Tool failed', ['error' => $e->getMessage()]);
267+
```
268+
269+
### 4. Input Validation
270+
271+
- Use readonly DTOs for immutability
272+
- Leverage PHP's type system and Spiral's validation attributes
273+
- Provide clear descriptions for AI context understanding
274+
- Set sensible defaults where appropriate
275+
276+
### 5. Route Naming
277+
278+
Follow the established pattern for route naming:
279+
280+
- Path: `/tools/call/{tool-name}`
281+
- Name: `tools.{category}.{action}` or `tools.{tool-name}`
282+
283+
## Tool Registration
284+
285+
While CTX uses attribute-based tool discovery, tools must be registered in the MCP server bootloader to be available. Follow these steps:
286+
287+
### 1. Register Your Tool Classes
288+
289+
Add your tool action classes to `src/McpServer/McpServerBootloader.php`:
290+
291+
```php
292+
// 1. Import your tool actions
293+
use YourNamespace\Action\Tools\YourCategory\YourToolAction;
294+
295+
// 2. Add to the actions() method
296+
if ($config->isYourCategoryEnabled()) {
297+
$actions = [
298+
...$actions,
299+
YourToolAction::class,
300+
];
301+
}
302+
```
303+
304+
### 2. Add Configuration Support (Optional)
305+
306+
For configurable tools, add settings to `McpConfig.php`:
307+
308+
```php
309+
// In the $config array
310+
'your_category' => [
311+
'enable' => true,
312+
'your_tool' => true,
313+
],
314+
315+
// Add configuration methods
316+
public function isYourCategoryEnabled(): bool
317+
{
318+
return $this->config['your_category']['enable'] ?? true;
319+
}
320+
```
321+
322+
### 3. Environment Variables (Optional)
323+
324+
Add environment variable support in `McpServerBootloader::init()`:
325+
326+
```php
327+
'your_category' => [
328+
'enable' => (bool) $env->get('MCP_YOUR_CATEGORY', true),
329+
'your_tool' => (bool) $env->get('MCP_YOUR_TOOL', true),
330+
],
331+
```
332+
333+
### Registration Examples
334+
335+
#### Simple Tool Registration
336+
For basic tools without configuration:
337+
338+
```php
339+
// In actions() method
340+
$actions = [
341+
...$actions,
342+
YourSimpleToolAction::class,
343+
];
344+
```
345+
346+
#### Configurable Tool Category
347+
For tool categories with multiple tools:
348+
349+
```php
350+
if ($config->isGitOperationsEnabled()) {
351+
$gitActions = [];
352+
353+
if ($config->isGitStatusEnabled()) {
354+
$gitActions[] = GitStatusAction::class;
355+
}
356+
357+
if ($config->isGitCommitEnabled()) {
358+
$gitActions[] = GitCommitAction::class;
359+
}
360+
361+
$actions = [...$actions, ...$gitActions];
362+
}
363+
```
364+
365+
### Tool Discovery
366+
367+
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.
368+
369+
## Testing Your Tool
370+
371+
1. **Start the MCP server**: `php ctx server`
372+
2. **List tools**: Use the `tools/list` endpoint to verify your tool appears
373+
3. **Test execution**: Call your tool through the MCP protocol
374+
4. **Check logs**: Monitor application logs for any issues
375+
376+
This modern approach provides type safety, automatic validation, and excellent IDE support while maintaining clean
377+
separation of concerns.

0 commit comments

Comments
 (0)