fix: emit a mimeType as declared - #262
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## trunk #262 +/- ##
============================================
- Coverage 88.02% 87.95% -0.07%
+ Complexity 1270 1258 -12
============================================
Files 54 54
Lines 4117 4085 -32
============================================
- Hits 3624 3593 -31
+ Misses 493 492 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR removes MIME type format/allow-list validation and updates the adapter to emit mimeType exactly as provided (subject to existing non-empty guards in some places), aligning behavior with MCP’s “opaque string” treatment of mimeType.
Changes:
- Removed MIME type validators and their call sites so
mimeTypeis no longer constrained by an RFC2045-style pattern or icon allow-list. - Adjusted resource/prompt/icon validation to check only presence/type where applicable (with
image/audiostill requiring amimeTypefield). - Updated PHPUnit coverage to reflect the new “accept any string” behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| includes/Domain/Utils/McpValidator.php | Drops icon MIME allow-list enforcement (type-only check). |
| includes/Domain/Resources/RegisterAbilityAsMcpResource.php | Emits ability _meta.mcp.mimeType when non-empty after trimming. |
| includes/Domain/Resources/McpResourceValidator.php | Removes MIME format validation; keeps type checking when provided. |
| includes/Domain/Resources/McpResource.php | Emits mimeType when non-empty (now without format validation). |
| includes/Domain/Prompts/McpPromptValidator.php | Requires mimeType for image/audio blocks without validating prefixes; loosens resource_link mimeType checks. |
| tests/phpunit/Unit/Resources/RegisterAbilityAsMcpResourceTest.php | Updates expectation to keep and emit unusual mimeType values. |
| tests/phpunit/Unit/Domain/Utils/McpValidatorTest.php | Removes MIME validator tests; updates icon validation expectations. |
| tests/phpunit/Unit/Domain/Resources/McpResourceValidatorTest.php | Updates resource validation tests to accept arbitrary mimeType strings. |
| tests/phpunit/Unit/Domain/Prompts/McpPromptValidatorTest.php | Updates prompt validation tests to accept arbitrary image/audio/resource_link mimeType strings while still requiring presence for image/audio. |
Comments suppressed due to low confidence (1)
includes/Domain/Prompts/McpPromptValidator.php:373
- Using empty($content['mimeType']) treats the string '0' as empty and will incorrectly fail validation even though it's a non-empty string. Use an isset/is_string/trim check instead to avoid empty() edge cases and align with the intended "non-empty string" requirement.
if ( empty( $content['mimeType'] ) || ! is_string( $content['mimeType'] ) ) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
MCP places no format constraint on `mimeType` on any object that carries one, so presence and type are the only checks that apply. Remove `validate_mime_type()`, `validate_image_mime_type()`, `validate_audio_mime_type()`, `validate_icon_mime_type()` and the icon MIME allow-list, along with their call sites in the resource and prompt validators, `McpResource` and `RegisterAbilityAsMcpResource`. A `mimeType` is now kept when it is a non-empty string and emitted as written. `image` and `audio` content blocks still require a `mimeType`, which the schema marks required there. A `mimeType` carrying RFC 2045 parameters, such as the `text/html;profile=mcp-app` an MCP Apps UI template declares, reaches the `resources/list` descriptor rather than being dropped. Removes four public static methods from `McpValidator`.
982a684 to
5f3ef18
Compare
gziolo
left a comment
There was a problem hiding this comment.
Code changes loog good.
Some minor tweaks are required before merge:
- outdated docs
mcp-adapter/docs/architecture/overview.md
Line 322 in f89c672
- the existing fixture needs some refresh, as it is valid per requirements, but unusual in
mcp-adapter/tests/phpunit/Fixtures/DummyAbility.php
Lines 916 to 938 in f89c672
## What? Closes #245. Carry `_meta` through to the DTOs the adapter emits, and normalize it at every site that builds one, so only a value a client can accept reaches the wire. - copy `_meta` from a handler's content item onto resource contents, embedded resources, content blocks and prompt messages - route every `_meta` through `McpValidator::normalize_meta()`, which yields a value only when it serializes as a JSON object - add a trailing, optional `resource_meta` argument to `ContentBlockHelper::embedded_text_resource()` and `embedded_blob_resource()`, which sets the nested resource contents' `_meta` while the existing argument sets the content block's - recognize a blob-only item as resource contents in `resources/read` - log a `_meta` the handlers drop, naming the object it sat on Second of three stacked PRs splitting #260, which carries the same change as one branch. **Based on #262**, so review that one first; the diff shown here is against it. Followed by #264. To exercise all three together, test #260. ## Why? `_meta` is the spec's channel for metadata that travels with a resource but is not its body. The adapter copied `uri`, `text`/`blob` and `mimeType` out of a handler's content item and left `_meta` behind, so MCP App UI resources (`ui://` with `text/html;profile=mcp-app`) reached the client with their HTML but none of the `_meta.ui` config the server attached. Fixing that alone would still emit metadata a client rejects. PHP represents a JSON object and a JSON array with one type, so an array check admits a list, and a list reaches the wire as a JSON array where MCP declares an object. A client validates `_meta` as part of the response that carries it, so a malformed value costs the whole payload rather than only itself. ## How? **Shape rule.** `McpValidator::normalize_meta()` returns a value only when it serializes as a JSON object; a non-array, an empty array or a list yields null. It returns null rather than raising, because `_meta` travels alongside a payload and a malformed one is not a reason to withhold the payload itself. **Embedded resources.** A tool may write the resource nested, under a `resource` key, or flat. The nested form addresses both `_meta` levels — outer for the block, inner for the contents. Strip `type` from the flat form and what remains is a `ResourceContents` literal, so its `_meta` describes the contents, which is what the same literal already means to `ResourcesHandler::create_content_dto()`. A caller who needs block-level `_meta` writes the nested form, which exists to express that distinction. **Resource contents detection.** `convert_contents_to_dtos()` distinguishes a single payload from a list of content items by looking in the first item for `uri`, `text` or `blob`. Binary contents carry no text and take the resource's own URI when they name none, so `blob` alone identifies one. **Where the generic path stops.** Only the two branches that read a `type` key — `resource` (inside the URI guard) and `image` — treat a sibling `_meta` as the content block's. The generic path returns the result verbatim as `structuredContent` and JSON-encodes it into a text block, so each key is already tool data; reading `_meta` off it would give one key two meanings. **Logging.** The handler sites route `_meta` through `HandlerHelperTrait::normalize_content_meta()`, which logs a warning naming the object the metadata sat on. It takes the raw value, because `normalize_meta()` answers null both for an absent `_meta` and for an unemittable one. A conforming client strips metadata it does not recognize and reports nothing, so this log is the only place the mistake surfaces. `ContentBlockHelper` is static and the domain factories build from config, so neither holds an error handler, and adding one would change signatures public since 0.5.0 — those sites normalize silently. **Documentation.** The user-facing guide for the shapes introduced here — structured resource contents, binary resources, and the MCP Apps route — lands in #264, because the same sections describe annotation and degradation behaviour that only exists after that PR. ### Behaviour changes - a `_meta` that cannot serialize as a JSON object is omitted rather than emitted as a JSON array; this reaches `ContentBlockHelper`, public API as of 0.5.0, though signatures are unchanged and only input that could never have serialized correctly is affected - a `resources/read` handler whose first item carries a `blob` and no `uri` or `text` returns one `BlobResourceContents` per item, rather than a single text block holding the list JSON-encoded - dropped `_meta` is logged with the object it sat on and the tool, prompt or URI it came from; the emitted payload is unchanged by the logging - the new `resource_meta` argument is trailing and optional, so existing calls are unaffected ### Use of AI Tools AI assistance: Yes Tool(s): Claude Code, Codex CLI Model(s): Claude Opus 5, Claude Fable 5, GPT-5.6 Sol Used for: Implementation and review. Every change was verified manually by me. ## Testing Instructions ### Resources 1. Register a resource whose handler returns a content item with `_meta`: ```php 'handler' => fn () => [[ 'uri' => 'ui://example/app', 'mimeType' => 'text/html;profile=mcp-app', 'text' => '<!doctype html>...', '_meta' => [ 'ui' => [ 'prefersBorder' => true ] ], ]], ``` Call `resources/read` and confirm `result.contents[0]._meta` is present. 2. Change that `_meta` to a list (`[ 'a', 'b' ]`) and confirm the key is absent from the response rather than emitted as a JSON array, and that a warning naming the resource is logged. 3. Return a handler payload whose first item carries only `blob` and `mimeType`, with no `uri` or `text`, and confirm `result.contents` holds one `BlobResourceContents` per item rather than a single text block with the list JSON-encoded. ### Tools 4. Register an ability with `meta.mcp._meta` set to an object and confirm it appears on that tool in `tools/list`. Set it to a list and confirm the key is omitted rather than emitted as a JSON array. 5. Return the **nested** embedded-resource shape from a tool and confirm each `_meta` lands on its own level — outer on the content block, inner on the resource contents: ```php [ 'type' => 'resource', 'resource' => [ 'uri' => 'ui://x', 'text' => '...', '_meta' => [ 'contents' => true ] ], '_meta' => [ 'block' => true ], ] ``` 6. Return the **flat** shape (`type`, `uri`, `mimeType`, `text`, `_meta`) and confirm its `_meta` lands on the resource contents, not the block. 7. Return a `type: "image"` result with a sibling `_meta` and confirm it reaches the image content block. The image bytes go in `results` as raw binary, which the handler base64-encodes. ### Prompts 8. Return a prompt message content block carrying a `_meta` object and confirm it survives to `prompts/get`. Change it to a list and confirm the key is omitted and a warning naming the prompt is logged. ### Gate 9. Run `composer test`, `composer lint` and `composer phpstan`. ## Changelog Entry > Fixed - Preserve `_meta` on resource contents, embedded resources, content blocks and prompt messages, and omit a `_meta` that would not serialize as a JSON object. --------- Co-authored-by: Grzegorz Ziolkowski <grzegorz@gziolo.pl>
What?
See #245.
Emit
mimeTypeas declared. MCP places no format constraint onmimeTypeon any object that carries one, so presence and type are the only checks that apply.validate_mime_type(),validate_image_mime_type(),validate_audio_mime_type(),validate_icon_mime_type()and the icon MIME allow-list, with their call sitesmimeTypewhen it is a non-empty string and emit the value as writtenimageandaudiocontent blocks still require amimeType, which the schema marks required thereFirst of three stacked PRs splitting #260, which carries the same change as one branch.
Review order is #262 → #263 → #264. To exercise all three together, test #260.
Why?
The adapter validated
mimeTypeagainst an RFC 2045 pattern that rejects parameters, and icons against a fixed allow-list. Neither constraint comes from MCP, which typesmimeTypeas a plain string everywhere it appears.The pattern drops any media type carrying a parameter.
text/html;profile=mcp-appis the media type an MCP Apps UI template declares, so a UI resource never reached theresources/listdescriptor with the type that identifies it.How?
The four validators are removed rather than relaxed: with the format constraint gone, each reduces to
is_string(), which the call sites already do.Icon
mimeTypekeeps its type check and loses the allow-list. The spec states which types a client must support; it does not restrict what a server may declare, and an icon a given client cannot render is that client's decision to make.McpResourceandRegisterAbilityAsMcpResourcekeep the'' !== $mime_typeguard, so an empty or whitespace-only value is still omitted rather than emitted as an empty string.Behaviour changes
mimeTypecarrying RFC 2045 parameters, such astext/html;profile=mcp-app, reaches theresources/listdescriptor instead of being droppedmimeTypestring now survives to the wire as written, on every object that carries oneimageandaudioblocks no longer require the type to start withimage/oraudio/; amimeTypeis still requiredpublic staticmethods are removed fromMcpValidatorUse of AI Tools
AI assistance: Yes
Tool(s): Claude Code, Codex CLI
Model(s): Claude Opus 5, Claude Fable 5, GPT-5.6 Sol
Used for: Implementation and review. Every change was verified manually by me.
Testing Instructions
meta.mcp.mimeTypeset totext/html;profile=mcp-appand confirmresources/listcarries that exact string on the descriptor, unaltered.mimeTypeisimage/avifand confirm the icon is kept rather than skipped.imageblock withmimeTypeset toapplication/octet-streamand confirm the message renders rather than failing validation. RemovemimeTypeentirely and confirm it still fails.composer test,composer lintandcomposer phpstan.Changelog Entry