Skip to content

Commit c8840af

Browse files
feat(schema): add Schema attributes and enhance DocBlock array type parsing
- Implement Schema attribute system for type-safe JSON Schema validation - Enhance DocBlock array type parsing for various notations: - string[], int[] (basic arrays) - array<T> (generic arrays) - array<array<T>> (nested arrays) - array{key: type} (object-like arrays) - Update SchemaGenerator to respect precedence: Schema > DocBlock > PHP types - Add comprehensive tests for new functionality
1 parent e059541 commit c8840af

File tree

10 files changed

+1222
-25
lines changed

10 files changed

+1222
-25
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This package simplifies building MCP servers through:
1414
* **Attribute-Based Definition:** Define MCP elements using PHP 8 Attributes (`#[McpTool]`, `#[McpResource]`, `#[McpPrompt]`, `#[McpResourceTemplate]`) on your methods or invokable classes.
1515
* **Manual Registration:** Programmatically register elements using a fluent builder API.
1616
* **Explicit Discovery:** Trigger attribute scanning on demand via the `$server->discover()` method.
17-
* **Metadata Inference:** Intelligently generate MCP schemas and descriptions from type hints and DocBlocks.
17+
* **Metadata Inference:** Intelligently generate MCP schemas and descriptions from PHP type hints and DocBlocks.
1818
* **Selective Caching:** Optionally cache *discovered* element definitions to speed up startup, while always preserving manually registered elements.
1919
* **Flexible Transports:** Supports `stdio` and `http+sse`, separating core logic from network communication.
2020
* **PSR Compliance:** Integrates with PSR-3 (Logging), PSR-11 (Container), and PSR-16 (SimpleCache).
@@ -138,6 +138,8 @@ The server uses a decoupled architecture:
138138
* **`Server`:** The central object holding the configured state and core logic components (`Registry`, `Processor`, `ClientStateManager`, `Configuration`). It's transport-agnostic. Provides methods to `discover()` elements and `listen()` via a specific transport.
139139
* **`Registry`:** Stores MCP element definitions. **Distinguishes between manually registered and discovered elements.** Handles optional caching of *discovered* elements only. Loads cached discovered elements upon instantiation if available.
140140
* **`Processor`:** Processes parsed JSON-RPC requests/notifications, executes handlers (via DI Container), formats results, handles errors.
141+
* **`SchemaGenerator`:** Generates JSON Schema for method parameters from PHP type hints and DocBlocks.
142+
* **`SchemaValidator`:** Validates incoming data against the generated JSON Schema.
141143
* **`ClientStateManager`:** Manages client runtime state (initialization, subscriptions, activity) using the configured cache.
142144
* **`ServerTransportInterface`:** Event-driven interface for server-side transports (`StdioServerTransport`, `HttpServerTransport`). Handles communication, emits events.
143145
* **`Protocol`:** Internal bridge listening to transport events, interacting with `Processor` and `ClientStateManager`.
@@ -324,6 +326,16 @@ The value returned by your method determines the content sent back to the client
324326

325327
The method's return type hint (`@return` tag in DocBlock) is used to generate the tool's output schema, but the actual formatting depends on the *value* returned at runtime.
326328

329+
**Schema Generation**
330+
331+
The server automatically generates JSON Schema for tool parameters based on:
332+
333+
1. PHP type hints
334+
2. DocBlock annotations
335+
3. Schema attributes (for enhanced validation)
336+
337+
**Examples:**
338+
327339
```php
328340
/**
329341
* Fetches user details by ID.
@@ -338,6 +350,18 @@ public function getUserById(int $userId, bool $includeEmail = false): array
338350
// ... implementation returning an array ...
339351
}
340352

353+
/**
354+
* Process user data with nested structures.
355+
*
356+
* @param array{name: string, contact: array{email: string, phone?: string}} $userData
357+
* @param string[] $tags Tags associated with the user
358+
* @return array{success: bool, message: string}
359+
*/
360+
#[McpTool]
361+
public function processUserData(array $userData, array $tags): array {
362+
// Implementation
363+
}
364+
341365
/**
342366
* Returns PHP code as formatted text.
343367
*
@@ -366,6 +390,33 @@ class AdderTool {
366390
}
367391
```
368392

393+
**Additional Validation with `#[Schema]`**
394+
395+
For enhanced schema generation and parameter validation, you can use the `Schema` attribute:
396+
397+
```php
398+
use PhpMcp\Server\Attributes\Schema;
399+
use PhpMcp\Server\Attributes\Schema\Format;
400+
use PhpMcp\Server\Attributes\Schema\ArrayItems;
401+
use PhpMcp\Server\Attributes\Schema\Property;
402+
403+
/**
404+
* Validates user information.
405+
*/
406+
#[McpTool]
407+
public function validateUser(
408+
#[Schema(format: 'email')]
409+
string $email,
410+
411+
#[Schema(minItems: 2, uniqueItems: true)]
412+
array $tags
413+
): bool {
414+
// Implementation
415+
}
416+
```
417+
418+
The Schema attribute adds JSON Schema constraints like string formats, numeric ranges, array constraints, and object property validations.
419+
369420
#### `#[McpResource]`
370421

371422
Marks a method **or an invokable class** as representing a specific, static MCP Resource instance. Resources represent pieces of content or data identified by a URI. The target method (or `__invoke`) will typically be called when a client performs a `resources/read` for the specified URI.

src/Attributes/Schema.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Attributes;
6+
7+
use Attribute;
8+
use PhpMcp\Server\Attributes\Schema\ArrayItems;
9+
use PhpMcp\Server\Attributes\Schema\Property;
10+
11+
#[Attribute(Attribute::TARGET_PARAMETER)]
12+
class Schema
13+
{
14+
/** @var Property[] */
15+
protected array $properties = [];
16+
17+
/**
18+
* @param string|null $format String format (email, date-time, uri, etc.)
19+
* @param int|null $minLength Minimum string length
20+
* @param int|null $maxLength Maximum string length
21+
* @param string|null $pattern Regular expression pattern
22+
* @param int|float|null $minimum Minimum numeric value
23+
* @param int|float|null $maximum Maximum numeric value
24+
* @param bool|null $exclusiveMinimum Whether minimum is exclusive
25+
* @param bool|null $exclusiveMaximum Whether maximum is exclusive
26+
* @param int|float|null $multipleOf Value must be multiple of this number
27+
* @param ArrayItems|null $items Schema for array items
28+
* @param int|null $minItems Minimum array items
29+
* @param int|null $maxItems Maximum array items
30+
* @param bool|null $uniqueItems Whether array items must be unique
31+
* @param Property[] $properties Properties for object validation
32+
* @param string[]|null $required Required properties for objects
33+
* @param bool|Schema|null $additionalProperties Whether additional properties are allowed
34+
* @param mixed|null $enum List of allowed values
35+
* @param mixed|null $default Default value
36+
*/
37+
public function __construct(
38+
public ?string $format = null,
39+
public ?int $minLength = null,
40+
public ?int $maxLength = null,
41+
public ?string $pattern = null,
42+
public int|float|null $minimum = null,
43+
public int|float|null $maximum = null,
44+
public ?bool $exclusiveMinimum = null,
45+
public ?bool $exclusiveMaximum = null,
46+
public int|float|null $multipleOf = null,
47+
public ?ArrayItems $items = null,
48+
public ?int $minItems = null,
49+
public ?int $maxItems = null,
50+
public ?bool $uniqueItems = null,
51+
array $properties = [],
52+
public ?array $required = null,
53+
public bool|Schema|null $additionalProperties = null,
54+
public mixed $enum = null,
55+
public mixed $default = null,
56+
) {
57+
$this->properties = $properties;
58+
}
59+
60+
/**
61+
* Convert to JSON Schema array
62+
*/
63+
public function toArray(): array
64+
{
65+
$schema = [];
66+
67+
// String constraints
68+
if ($this->format !== null) $schema['format'] = $this->format;
69+
if ($this->minLength !== null) $schema['minLength'] = $this->minLength;
70+
if ($this->maxLength !== null) $schema['maxLength'] = $this->maxLength;
71+
if ($this->pattern !== null) $schema['pattern'] = $this->pattern;
72+
73+
// Numeric constraints
74+
if ($this->minimum !== null) $schema['minimum'] = $this->minimum;
75+
if ($this->maximum !== null) $schema['maximum'] = $this->maximum;
76+
if ($this->exclusiveMinimum !== null) $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
77+
if ($this->exclusiveMaximum !== null) $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
78+
if ($this->multipleOf !== null) $schema['multipleOf'] = $this->multipleOf;
79+
80+
// Array constraints
81+
if ($this->items !== null) $schema['items'] = $this->items->toArray();
82+
if ($this->minItems !== null) $schema['minItems'] = $this->minItems;
83+
if ($this->maxItems !== null) $schema['maxItems'] = $this->maxItems;
84+
if ($this->uniqueItems !== null) $schema['uniqueItems'] = $this->uniqueItems;
85+
86+
// Object constraints
87+
if (!empty($this->properties)) {
88+
$props = [];
89+
foreach ($this->properties as $property) {
90+
$props[$property->name] = $property->toArray();
91+
}
92+
$schema['properties'] = $props;
93+
}
94+
95+
if ($this->required !== null) $schema['required'] = $this->required;
96+
97+
if ($this->additionalProperties !== null) {
98+
if ($this->additionalProperties instanceof self) {
99+
$schema['additionalProperties'] = $this->additionalProperties->toArray();
100+
} else {
101+
$schema['additionalProperties'] = $this->additionalProperties;
102+
}
103+
}
104+
105+
// General constraints
106+
if ($this->enum !== null) $schema['enum'] = $this->enum;
107+
if ($this->default !== null) $schema['default'] = $this->default;
108+
109+
return $schema;
110+
}
111+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Attributes\Schema;
6+
7+
use PhpMcp\Server\Attributes\Schema;
8+
9+
/**
10+
* Array item schema - extends Schema to inherit all schema properties
11+
*/
12+
class ArrayItems extends Schema
13+
{
14+
/**
15+
* Creates a schema definition for array items
16+
*/
17+
public function __construct(
18+
?string $format = null,
19+
?int $minLength = null,
20+
?int $maxLength = null,
21+
?string $pattern = null,
22+
int|float|null $minimum = null,
23+
int|float|null $maximum = null,
24+
?bool $exclusiveMinimum = null,
25+
?bool $exclusiveMaximum = null,
26+
int|float|null $multipleOf = null,
27+
?ArrayItems $items = null,
28+
?int $minItems = null,
29+
?int $maxItems = null,
30+
?bool $uniqueItems = null,
31+
array $properties = [],
32+
?array $required = null,
33+
bool|Schema|null $additionalProperties = null,
34+
mixed $enum = null,
35+
mixed $default = null,
36+
) {
37+
parent::__construct(
38+
format: $format,
39+
minLength: $minLength,
40+
maxLength: $maxLength,
41+
pattern: $pattern,
42+
minimum: $minimum,
43+
maximum: $maximum,
44+
exclusiveMinimum: $exclusiveMinimum,
45+
exclusiveMaximum: $exclusiveMaximum,
46+
multipleOf: $multipleOf,
47+
items: $items,
48+
minItems: $minItems,
49+
maxItems: $maxItems,
50+
uniqueItems: $uniqueItems,
51+
properties: $properties,
52+
required: $required,
53+
additionalProperties: $additionalProperties,
54+
enum: $enum,
55+
default: $default,
56+
);
57+
}
58+
}

src/Attributes/Schema/Format.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Attributes\Schema;
6+
7+
/**
8+
* Common string formats supported by JSON Schema
9+
*/
10+
class Format
11+
{
12+
// String formats
13+
public const DATE = 'date';
14+
public const TIME = 'time';
15+
public const DATE_TIME = 'date-time';
16+
public const DURATION = 'duration';
17+
public const EMAIL = 'email';
18+
public const IDN_EMAIL = 'idn-email';
19+
public const HOSTNAME = 'hostname';
20+
public const IDN_HOSTNAME = 'idn-hostname';
21+
public const IPV4 = 'ipv4';
22+
public const IPV6 = 'ipv6';
23+
public const URI = 'uri';
24+
public const URI_REFERENCE = 'uri-reference';
25+
public const IRI = 'iri';
26+
public const IRI_REFERENCE = 'iri-reference';
27+
public const URI_TEMPLATE = 'uri-template';
28+
public const JSON_POINTER = 'json-pointer';
29+
public const RELATIVE_JSON_POINTER = 'relative-json-pointer';
30+
public const REGEX = 'regex';
31+
public const UUID = 'uuid';
32+
}

src/Attributes/Schema/Property.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Server\Attributes\Schema;
6+
7+
use PhpMcp\Server\Attributes\Schema;
8+
9+
/**
10+
* Property definition for object schemas
11+
*/
12+
class Property extends Schema
13+
{
14+
/**
15+
* @param string $name Property name
16+
*/
17+
public function __construct(
18+
public string $name,
19+
?string $format = null,
20+
?int $minLength = null,
21+
?int $maxLength = null,
22+
?string $pattern = null,
23+
int|float|null $minimum = null,
24+
int|float|null $maximum = null,
25+
?bool $exclusiveMinimum = null,
26+
?bool $exclusiveMaximum = null,
27+
int|float|null $multipleOf = null,
28+
?ArrayItems $items = null,
29+
?int $minItems = null,
30+
?int $maxItems = null,
31+
?bool $uniqueItems = null,
32+
array $properties = [],
33+
?array $required = null,
34+
bool|Schema|null $additionalProperties = null,
35+
mixed $enum = null,
36+
mixed $default = null,
37+
) {
38+
parent::__construct(
39+
format: $format,
40+
minLength: $minLength,
41+
maxLength: $maxLength,
42+
pattern: $pattern,
43+
minimum: $minimum,
44+
maximum: $maximum,
45+
exclusiveMinimum: $exclusiveMinimum,
46+
exclusiveMaximum: $exclusiveMaximum,
47+
multipleOf: $multipleOf,
48+
items: $items,
49+
minItems: $minItems,
50+
maxItems: $maxItems,
51+
uniqueItems: $uniqueItems,
52+
properties: $properties,
53+
required: $required,
54+
additionalProperties: $additionalProperties,
55+
enum: $enum,
56+
default: $default,
57+
);
58+
}
59+
}

0 commit comments

Comments
 (0)