Skip to content

Conversation

@galatanovidiu
Copy link
Contributor

Summary

  • Add 187 generated PHP files for the Model Context Protocol (MCP) specification
  • Generated from official MCP TypeScript schema using the TypeScript generator
  • PHP 7.4 compatible with PHPStan max level compliance

What's included

Type Count Description
DTOs 143 Data Transfer Objects for all MCP protocol types
Enums 4 PHP 7.4 class-based enums for string literal unions
Unions 15 Interfaces with discriminator support for union types
Factories 11 Factory classes for union type hydration from arrays

Structure

  • src/Common/ - Shared base classes, traits, JSON-RPC, protocol types
  • src/Server/ - Server-side types (Tools, Resources, Prompts, Logging, Lifecycle)
  • src/Client/ - Client-side types (Sampling, Elicitation, Roots, Tasks)

Key features

  • Full fromArray()/toArray() serialization support
  • Proper inheritance hierarchy matching MCP specification
  • Type-safe discriminator-based union handling
  • PHPDoc array shapes for IDE autocomplete
  • Version tracking annotations (@since, @last-updated)

Test plan

  • PHPStan passes at max level
  • Manual verification of DTO structure against MCP specification

  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.
@swissspidy
Copy link
Member

  • Generated from official MCP TypeScript schema using the TypeScript generator

Thanks for putting the generator together! The https://github.com/modelcontextprotocol/php-sdk folks might be interested in that too.

PHP 7.4 compatible with PHPStan max level compliance

Is this part of the generation or as part of a post-processing cleanup?

  • Manual verification of DTO structure against MCP specification

Any thoughts on how we can achieve automated verification?

* @mcp-subdomain Core
* @mcp-version 2025-11-25
*/
class CompleteResultCompletion extends AbstractDataTransferObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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)

Copy link
Contributor Author

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 limit
  • total (int|null) - optional
  • hasMore (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
Copy link
Member

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.

Copy link
Contributor Author

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:

  1. A MAX_VALUES = 100 constant on CompleteResultCompletion for discoverability
  2. Runtime validation in fromArray() that throws InvalidArgumentException when 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`.
@JasonTheAdams
Copy link
Member

Looking at the structure, I'm tempted to break things down into a structure such as:

/DTO
    - /Request
    - /Result

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).
@galatanovidiu
Copy link
Contributor Author

Looking at the structure, I'm tempted to break things down into a structure such as:

/DTO
    - /Request
    - /Result

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.

I prefer simplicity, but I do not mind grouping requests and results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants