Skip to content

[Schema] Add resource_link content block#400

Open
cancan101 wants to merge 2 commits into
modelcontextprotocol:mainfrom
cancan101:feat/resource-link-content
Open

[Schema] Add resource_link content block#400
cancan101 wants to merge 2 commits into
modelcontextprotocol:mainfrom
cancan101:feat/resource-link-content

Conversation

@cancan101

Copy link
Copy Markdown

Summary

Implements the spec's resource_link content type (protocol revision 2025-06-18, still present in 2025-11-25), which was missing from the SDK: src/Schema/Content/ had TextContent, ImageContent, AudioContent, and EmbeddedResource, 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.:

interface ResourceLink extends Resource {
  type: "resource_link";
}
interface Resource extends BaseMetadata, Icons {
  uri: string;
  description?: string;
  mimeType?: string;
  annotations?: Annotations;
  size?: number;
  _meta?: { [key: string]: unknown };
}

Mcp\Schema\Content\ResourceLink mirrors this exactly: required uri + name, optional title, description, mimeType, annotations, size, icons, _meta — following the same constructor/fromArray/jsonSerialize conventions as the sibling EmbeddedResource and Mcp\Schema\ResourceDefinition classes.

Registration points touched

resource_link is now accepted everywhere resource (EmbeddedResource) content blocks are (de)serialized by type, matching the spec's ContentBlock union (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 / CreateSamplingMessageResult are intentionally left untouched — the spec's SamplingMessageContentBlock union does not include ResourceLink.

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/fromArray round-trip, invalid-type/missing-field/invalid-_meta rejection.
  • tests/Unit/Schema/Content/PromptMessageTest.php: resource_link deserialization, serialization, round-trip, and constructor acceptance.
  • tests/Unit/Schema/Result/CallToolResultTest.php: resource_link deserialization 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_link through both the PromptMessage-instance path and the raw-array role/content path.

Full unit suite (841 tests / 2557 assertions), php-cs-fixer --dry-run, phpstan analyse, composer validate --strict, and make docs all 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_link block 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

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>
@chr-hertel chr-hertel changed the title Add resource_link content block [Schema] Add resource_link content block Jul 21, 2026
@chr-hertel chr-hertel added Schema Issues & PRs related to the Schema component improves spec compliance Improves consistency with other SDKs such as TyepScript labels Jul 21, 2026
@chr-hertel

Copy link
Copy Markdown
Member

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>
@cancan101

Copy link
Copy Markdown
Author

Good call — pushed a commit that extends the discovery-userprofile example: lookup_user looks up a user by ID and returns a resource_link pointing at the existing user://{userId}/profile resource template instead of embedding it, mirroring the search-hit-referencing-a-resource use case from the PR description.

This is now exercised by that example's inspector snapshot tests (tools_list, tools_call-lookup_user, tools_call-lookup_user_not_found), and it actually caught a real bug: ToolReference::extractStructuredContent() was passing the raw [TextContent, ResourceLink] return straight through as structuredContent, which serializes to a JSON list — the inspector's MCP client rejected the tool call outright since the spec requires structuredContent to be an object. Fixed by skipping structured-content extraction whenever the raw result contains a Content instance, since those are already reflected in content. Added a unit test for that in RegistryTest.

Ran the full suite locally (Node 20+ for inspector, since 18 can't npx it): unit suite (842 tests), the full tests/Inspector suite, php-cs-fixer, phpstan, and composer validate --strict all green.

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

Labels

improves spec compliance Improves consistency with other SDKs such as TyepScript Schema Issues & PRs related to the Schema component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing resource_link content block: tool results cannot reference resources without embedding them

2 participants