[Schema] Add resource_link content block#400
Conversation
The spec's `resource_link` content type (protocol revision 2025-06-18, still present in 2025-11-25) had no corresponding schema class: it lets a tool result or prompt message reference a resource by URI/name without embedding its full contents, the way `EmbeddedResource` does. Without it, tools returning many or large resource references (e.g. search results over a DAM) are stuck smuggling URIs through text/structured content. Add `Mcp\Schema\Content\ResourceLink`, mirroring the spec's `Resource` shape (uri, name, title, description, mimeType, annotations, size, icons, _meta) with a fixed `type: "resource_link"`, following the conventions of the sibling `EmbeddedResource`/`ResourceDefinition` classes. Register it everywhere `resource` content blocks are (de)serialized by type per the spec's `ContentBlock` union: - `CallToolResult::fromArray()` - `PromptMessage` (constructor type, `fromArray()`, phpstan types) - `PromptResultFormatter` (typed-array and direct-instance paths) `SamplingMessage`/`CreateSamplingMessageResult` are intentionally left unchanged: the spec's `SamplingMessageContentBlock` does not include `ResourceLink`. Fixes modelcontextprotocol#399 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hi @cancan101, thanks for working on it and happy to hear Pimcore is adopting MCP 😎 👍 one thing to add, before i jump into review, can you find a meaningful addition to one of our examples here? getting stuff into the examples also brings in functional test coverage via inspector :) |
Add a lookup_user tool to the discovery-userprofile example: given a
userId, it returns a short text summary plus a resource_link pointing
at the existing user://{userId}/profile resource template, instead of
embedding the profile. This mirrors the PR's motivating use case (a
tool referencing many/large resources by URI rather than inlining
them) and exercises resource_link through the example's own inspector
snapshot tests (tools_call-lookup_user[_not_found], tools_list).
Wiring this up surfaced a real bug: ToolReference::extractStructuredContent()
passed any raw array return value straight through as structuredContent,
even when it held Content instances (e.g. the TextContent/ResourceLink
pair above). That serializes to a JSON list, which fails the spec's
requirement that structuredContent be an object - the inspector's MCP
client rejected the tool call outright until fixed. Skip structuredContent
extraction whenever the raw result contains a Content item, since those
are already represented in `content` and were never meant to double as
structured data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Good call — pushed a commit that extends the This is now exercised by that example's inspector snapshot tests ( Ran the full suite locally (Node 20+ for inspector, since 18 can't |
Summary
Implements the spec's
resource_linkcontent type (protocol revision 2025-06-18, still present in 2025-11-25), which was missing from the SDK:src/Schema/Content/hadTextContent,ImageContent,AudioContent, andEmbeddedResource, but no way to reference a resource by URI/name without embedding its full contents.Fixes #399
Spec revision implemented
Per the MCP schema,
ResourceLink extends Resource(BaseMetadata+Icons), i.e.:Mcp\Schema\Content\ResourceLinkmirrors this exactly: requireduri+name, optionaltitle,description,mimeType,annotations,size,icons,_meta— following the same constructor/fromArray/jsonSerializeconventions as the siblingEmbeddedResourceandMcp\Schema\ResourceDefinitionclasses.Registration points touched
resource_linkis now accepted everywhereresource(EmbeddedResource) content blocks are (de)serialized bytype, matching the spec'sContentBlockunion (TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource):Mcp\Schema\Result\CallToolResult::fromArray()Mcp\Schema\Content\PromptMessage(constructor type union,fromArray(), phpstan types)Mcp\Capability\Formatter\PromptResultFormatter(both the typed-array path and the direct-Content-instance path)SamplingMessage/CreateSamplingMessageResultare intentionally left untouched — the spec'sSamplingMessageContentBlockunion does not includeResourceLink.Also added a doc example (
docs/mcp-elements.md) and a CHANGELOG entry.Test coverage
tests/Unit/Schema/Content/ResourceLinkTest.php: construction, optional-field defaults,jsonSerialize/fromArrayround-trip, invalid-type/missing-field/invalid-_metarejection.tests/Unit/Schema/Content/PromptMessageTest.php:resource_linkdeserialization, serialization, round-trip, and constructor acceptance.tests/Unit/Schema/Result/CallToolResultTest.php:resource_linkdeserialization alongside other content types in a single result, serialization, and a full round-trip mixing all five content types.tests/Unit/Capability/Formatter/PromptResultFormatterTest.php:resource_linkthrough both thePromptMessage-instance path and the raw-arrayrole/contentpath.Full unit suite (841 tests / 2557 assertions),
php-cs-fixer --dry-run,phpstan analyse,composer validate --strict, andmake docsall pass locally.Downstream use case
pimcore/data-hub-simple-rest#287 — a Pimcore DAM MCP server whose search/tree tools return hits referencing image/video assets that are separately readable via registered resource templates. Per spec, each hit should carry a
resource_linkblock with the template-constructed URI; embedding blobs in a 25-hit search result isn't viable, so tool output was stuck smuggling URIs through text/structured content until this type existed.🤖 Generated with Claude Code