-
Notifications
You must be signed in to change notification settings - Fork 0
Add generated PHP DTOs for MCP schema version 2025-11-25 #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
Generated 187 PHP files including: - 143 DTOs for MCP protocol types - 4 Enums for string literal unions - 15 Union interfaces with discriminator support - 11 Factory classes for union type hydration PHP 7.4 compatible with PHPStan max level compliance.
Thanks for putting the generator together! The https://github.com/modelcontextprotocol/php-sdk folks might be interested in that too.
Is this part of the generation or as part of a post-processing cleanup?
Any thoughts on how we can achieve automated verification? |
| * @mcp-subdomain Core | ||
| * @mcp-version 2025-11-25 | ||
| */ | ||
| class CompleteResultCompletion extends AbstractDataTransferObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
FYI, I'm working on this, and I will come back with more details
I've also spotted a few other problems that I'm addressing right now
The details will be on the commits (lots of them, I'm afraid)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was addressed in commit 1e8eb64.
The CompleteResultCompletion class now documents the exact shape matching the MCP
spec and php-sdk reference:
values(array) - required, with runtime validation for the 100-item limittotal(int|null) - optionalhasMore(bool|null) - optional
See CompleteResultCompletion
The generator fix has resolved the problem for other DTO's also
| * @mcp-subdomain Core | ||
| * @mcp-version 2025-11-25 | ||
| */ | ||
| class CompleteResult extends Result implements ServerResultInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing some spot checks.
As per the documentation, there's a limit of 100 items per response.
It's something the PHP SDK handles: https://github.com/modelcontextprotocol/php-sdk/blob/369932378f7bcfa29a00efc1530b70c4d501a24b/src/Schema/Result/CompletionCompleteResult.php#L34-L36
Is this reflected in the spec too so that we could include this as part of the generation?
If not, perhaps we could ask for it to be added?
Otherwise there is a gap in the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was addressed in commit 9397925.
The constraint is documented in the TypeScript schema's JSDoc as "Must not exceed 100 items". The generator extracts this pattern and generates:
- A
MAX_VALUES = 100constant on CompleteResultCompletion for discoverability - Runtime validation in
fromArray()that throwsInvalidArgumentExceptionwhen exceeded
The validation lives in CompleteResultCompletion (the nested object), which is where the values array property is defined - matching the structure in the php-sdk reference.
Unlike PHP SDK the validation is done inside fromArray as all validation is done there, and I have mixed feelings about this. I was thinking of moving all validation inside the constructor or making the constructor private. I decided to let this open for debate in the end.
Note: The extraction is currently pattern-based (looking for "Must not exceed N items" in JSDoc). This works for the current schema but isn't a formal constraint system. I'd prefer to keep it simple for now since MCP is now under AAIF authority and we don't know what schema changes may come - they might add proper JSON Schema constraints like maxItems in the future. In any case, as soon as a new protocol is released, we will need to review the generated code and likely make improvements in the generator.
The synthetic DTO extractor was generating empty classes for inline object types (like CompleteResultCompletion) when the TypeScript schema included JSDoc comments on properties. The regex expected property strings to start with the property name, but JSDoc comments caused the pattern to fail. Now the parser strips JSDoc comments before matching property names and extracts description text for PHP docblocks.
Optional array properties were calling asArray() which throws InvalidArgumentException when the value is null. This broke DTOs like CompleteRequestParamsContext where omitting the optional 'arguments' field caused a runtime exception.
\The generator now uses ${suffix} variable ('OrNull' for optional, '' for required) consistently in the "Array of primitives" code path, matching other type helpers like asObject${suffix} and asStringArray${suffix}.
The MCP spec defines ProgressToken as `string | number`, but the PHP DTOs accepted any value without runtime validation. This could produce invalid MCP payloads when arrays, booleans, or objects were passed. Add asStringOrNumber() and asStringOrNumberOrNull() helpers to the ValidatesRequiredFields trait, and update the DTO generator to detect string|number union types and use these helpers automatically.
The generator now preserves value types from TypeScript index signatures like `{ [key: string]: string }` and generates appropriate PHP validation.
- Add isIndexSignature and indexSignatureValueType fields to PhpType
- Add extractIndexSignatureValueType() to parse index signature value types
- Add asStringMap/asStringMapOrNull helpers for string-valued maps
- Add asObjectMap/asObjectMapOrNull helpers for object-valued maps
- Update DTO generator to use appropriate helpers based on value type
Affected types:
- { [key: string]: string } → array<string, string> with asStringMap()
- { [key: string]: object } → array<string, object> with asObjectMap()
- { [key: string]: unknown } → array<string, mixed> with asArray()
…letion.values The MCP spec states that completion values "Must not exceed 100 items". This adds automatic extraction of maxItems constraints from JSDoc descriptions and generates validation in fromArray(). - Add maxItems field to PhpProperty type - Extract limits from JSDoc pattern "Must not exceed N items" - Generate MAX_* constants for discoverable limits - Generate validation that throws InvalidArgumentException when exceeded
… type wrappers The renderFromArrayArg method always called asArray() for array types, ignoring the isOptional flag. This caused InvalidArgumentException at runtime when optional array properties like _meta were null.
Strip leading `$` from property names for PHP while preserving it for JSON keys. DtoGenerator had this via getPropertyNames(), but IntersectionTypeWrapperGenerator used prop.name directly, causing invalid syntax like `$this->$$schema`.
|
Looking at the structure, I'm tempted to break things down into a structure such as: I notice there are a lot of request and result objects, and it could be helpful to group them together. I'm very much open to pushback, though. |
… JSON
**Why:**
Downstream json_encode() / wp_json_encode() of DTO ->toArray() output produced {} placeholders when nested DTO objects leaked into arrays (protected props serialize as empty objects).
This broke MCP client-visible payloads for embedded resources and resource reads (baseline spec 2025-11-25).
**What:**
Update generator getSerializationExpression() to deep-serialize:
untyped union DTO properties (protected $x; with PHPDoc \WP\McpSchema\...\A|\WP\McpSchema\...\B) via runtime is_object(...) && method_exists(...,'toArray') ? ->toArray() : value
arrays of DTO unions (e.g. array<A|B> with no single arrayItemType) via array_map(... ->toArray() ...) with the same runtime guard.
Regenerate src/ for config 2025-11-25, fixing:
EmbeddedResource::toArray() to serialize resource as an array
ReadResourceResult::toArray() to serialize contents as an array of arrays
same pattern in other affected union-based DTOs (e.g. sampling message content, completion refs).
I prefer simplicity, but I do not mind grouping requests and results. |
Summary
What's included
Structure
src/Common/- Shared base classes, traits, JSON-RPC, protocol typessrc/Server/- Server-side types (Tools, Resources, Prompts, Logging, Lifecycle)src/Client/- Client-side types (Sampling, Elicitation, Roots, Tasks)Key features
fromArray()/toArray()serialization support@since,@last-updated)Test plan