Skip to content

Support mixed content types from the mcp server tool call response#2517

Merged
crivetimihai merged 6 commits intomainfrom
2512_toolcall_result_validation
Jan 27, 2026
Merged

Support mixed content types from the mcp server tool call response#2517
crivetimihai merged 6 commits intomainfrom
2512_toolcall_result_validation

Conversation

@kevalmahajan
Copy link
Copy Markdown
Member

@kevalmahajan kevalmahajan commented Jan 27, 2026

🐛 Bug-fix PR

Closes #2512

📌 Summary

Fixed tool invocation failures for tools that return complex content types (like ResourceLink, ImageContent) or contain Pydantic-specific types like AnyUrl. It ensures robust handling of diverse tool outputs in the Gateway.

🔁 Reproduction Steps

  1. Register a tool that returns a resource_link.
  2. Invoke the tool via the Gateway.
  3. Result:
    Initially fails with ValidationError (Input should be a valid string, received AnyUrl).
    Attempting to fix validation reveals AttributeError: 'ResourceLink' object has no attribute 'text'.

🐞 Root Cause

  1. tool_service.py: usage of .model_dump() without mode='json' preserved pydantic.AnyUrl objects, violating the internal model's str type constraints.
  2. streamablehttp_transport.py: The code blindly iterated over results assuming types.TextContent, accessing .text on every item, which crashed for ResourceLink or ImageContent.

#2512 (comment)

💡 Fix Description

  1. Serialization: Updated tool_service.py to use model_dump(by_alias=True, mode='json'), forcing conversion of AnyUrl and other types to their JSON-compatible primitives (strings).
  2. Content Handling: Refactored streamablehttp_transport.py to inspect content.type and correctly map to types.TextContent, types.ImageContent, or pass the dictionary dump for resource_link and other types, ensuring full protocol compatibility.

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 90 % make coverage
Manual regression no longer fails steps / screenshots

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed

@crivetimihai crivetimihai force-pushed the 2512_toolcall_result_validation branch from cdcddf0 to 8d08aaa Compare January 27, 2026 14:05
@crivetimihai
Copy link
Copy Markdown
Member

Code Review & Fixes Applied

I've reviewed this PR and found a critical bug in the original implementation that would have caused resource_link content to be converted to plain text. Here's what was fixed:

Issues Found

  1. Critical: Dict return for ResourceLink loses content type

    • Original code returned content.model_dump() (a dict) for resource_link
    • The MCP SDK's _convert_to_content() converts dicts to TextContent with JSON as text
    • This means ResourceLink would become plain text, losing the proper content type
  2. Missing content type handlers

    • audio (AudioContent) was not handled
    • resource (EmbeddedResource) was not handled
  3. Incomplete return type annotation

    • Missing AudioContent and ResourceLink in the function signature

Changes Made

  • tool_service.py: ✅ Original mode="json" fix is correct for AnyUrl serialization
  • streamablehttp_transport.py: Fixed to return proper MCP SDK types instead of dicts:
    • texttypes.TextContent
    • imagetypes.ImageContent
    • audiotypes.AudioContent
    • resource_linktypes.ResourceLink
    • resourcetypes.EmbeddedResource
    • Unknown → Fallback to TextContent with JSON string

Verification

  • All 173 relevant unit tests pass
  • Ruff/Black checks pass
  • Verified MCP SDK correctly preserves content types when returning proper SDK objects

Commits squashed and rebased onto main. Will do integration testing next.

… response

Closes #2512

This fix addresses tool invocation failures for tools that return complex
content types (like ResourceLink, ImageContent, AudioContent) or contain
Pydantic-specific types like AnyUrl.

Root causes fixed:
1. tool_service.py: Usage of model_dump() without mode='json' preserved
   pydantic.AnyUrl objects, violating internal model's str type constraints.
2. streamablehttp_transport.py: Code blindly assumed types.TextContent,
   accessing .text on every item, which crashed for ResourceLink or ImageContent.

Changes:
- Updated tool_service.py to use model_dump(by_alias=True, mode='json'),
  forcing conversion of AnyUrl to JSON-compatible strings.
- Refactored streamablehttp_transport.py to inspect content.type and correctly
  map to proper MCP SDK types (TextContent, ImageContent, AudioContent,
  ResourceLink, EmbeddedResource) ensuring full protocol compatibility.
- Updated return type annotation to include all MCP content types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai force-pushed the 2512_toolcall_result_validation branch from 8d08aaa to 30071fe Compare January 27, 2026 14:17
Addresses dropped metadata fields identified in PR #2517 review:
- Preserve annotations and _meta for TextContent, ImageContent, AudioContent
- Preserve size and _meta for ResourceLink (critical for file metadata)
- Handle EmbeddedResource via model_validate

Add comprehensive regression tests for:
- Mixed content types (text, image, audio, resource_link, embedded)
- Metadata preservation (annotations, _meta, size)
- Unknown content type fallback
- Missing optional metadata handling

Closes #2512

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Additional Fixes (Commit 57d7728)

Addressed the review findings regarding metadata preservation:

Changes

  1. Metadata preservation in call_tool (streamablehttp_transport.py:531-580)

    • TextContent: Now preserves annotations and _meta
    • ImageContent: Now preserves annotations and _meta
    • AudioContent: Now preserves annotations and _meta
    • ResourceLink: Now preserves description, mimeType, size, and _meta
    • EmbeddedResource: Handled via model_validate for complex nested structure
  2. Regression tests (9 new tests)

    • test_call_tool_with_image_content - ImageContent with mimeType and metadata
    • test_call_tool_with_audio_content - AudioContent with mimeType and metadata
    • test_call_tool_with_resource_link_content - ResourceLink with all fields including size
    • test_call_tool_with_embedded_resource_content - EmbeddedResource handling
    • test_call_tool_with_mixed_content_types - Multiple content types in one response
    • test_call_tool_preserves_text_metadata - TextContent annotations and _meta
    • test_call_tool_handles_unknown_content_type - Graceful fallback to TextContent
    • test_call_tool_handles_missing_optional_metadata - Handles None metadata
    • test_call_tool_resource_link_preserves_all_fields - Critical regression test for size

Test Results

All 74 tests in test_streamablehttp_transport.py pass, including the 9 new regression tests.

…tibility

mcpgateway.common.models.Annotations is a different Pydantic class from
mcp.types.Annotations. Passing gateway Annotations directly to MCP SDK
types causes ValidationError at runtime when real MCP responses include
annotations.

Fix:
- Add _convert_annotations() helper to convert gateway Annotations to dict
- Add _convert_meta() helper for consistent meta handling
- Apply conversion to all content types (text, image, audio, resource_link)

Add regression tests using actual gateway model types:
- test_call_tool_with_gateway_model_annotations
- test_call_tool_with_gateway_model_image_annotations

These tests use mcpgateway.common.models.TextContent/ImageContent with
mcpgateway.common.models.Annotations to verify the conversion works.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Fix: Annotations Type Mismatch (Commit c7376f2)

Addressed the annotations type compatibility issue identified in review:

Problem

  • mcpgateway.common.models.Annotationsmcp.types.Annotations
  • Passing gateway Annotations directly to MCP SDK types causes ValidationError
  • Any tool response with annotations would fail and call_tool would return []

Solution

Added helper functions to convert gateway types to dict before passing to MCP SDK:

def _convert_annotations(ann: Any) -> dict[str, Any] | None:
    if ann is None:
        return None
    if isinstance(ann, dict):
        return ann
    if hasattr(ann, "model_dump"):
        return ann.model_dump(by_alias=True, mode="json")
    return None

Applied to all content types: TextContent, ImageContent, AudioContent, ResourceLink.

New Regression Tests

  • test_call_tool_with_gateway_model_annotations - Uses actual mcpgateway.common.models.TextContent with mcpgateway.common.models.Annotations
  • test_call_tool_with_gateway_model_image_annotations - Uses actual mcpgateway.common.models.ImageContent with gateway Annotations

These tests verify the conversion works with real gateway types, not just mock dicts.

Test Results

All 76 tests pass + 22 doctests pass.

Add explicit tests for the AnyUrl serialization fix (Issue #2512 root cause):
- test_anyurl_serialization_without_mode_json - demonstrates the problem
- test_anyurl_serialization_with_mode_json - verifies the fix
- test_resource_link_anyurl_serialization - ResourceLink uri field
- test_tool_result_with_resource_link_serialization - ToolResult with ResourceLink
- test_mixed_content_with_anyurl_serialization - mixed content types

These tests verify that mode='json' in model_dump() correctly serializes
AnyUrl objects to strings, preventing validation errors when content is
passed to MCP SDK types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

AnyUrl Serialization Tests Added (Commit e841781)

Added explicit tests for the mode='json' fix that addresses the root cause of Issue #2512:

New Tests in test_tool_service.py

Test Purpose
test_anyurl_serialization_without_mode_json Demonstrates AnyUrl stays as object without fix
test_anyurl_serialization_with_mode_json Verifies mode='json' serializes AnyUrl to string
test_resource_link_anyurl_serialization ResourceLink uri field serialization
test_tool_result_with_resource_link_serialization ToolResult containing ResourceLink
test_mixed_content_with_anyurl_serialization Mixed content types with AnyUrl

These tests explicitly cover the model_dump(by_alias=True, mode="json") code path at tool_service.py:3192.

Summary of All Commits

  1. 30071fe32 - Initial fix: return MCP SDK types instead of dicts
  2. 57d772800 - Metadata preservation (annotations, _meta, size)
  3. c7376f2b1 - Annotations type conversion (gateway → dict → MCP SDK)
  4. e84178119 - AnyUrl serialization tests

All residual risks from review have been addressed.

…meta

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Integration Test Report

Timestamp: 2026-01-27T14:59:42 UTC
Environment: docker-compose on localhost:8080 (rebuilt with latest code)

Test Results: 17/17 PASSED ✅

Category Test Result
REST API Gateway Health Check
List Virtual Servers (1 server)
List MCP Gateways (2 gateways)
List Tools
List Resources
MCP Protocol Session Initialize
tools/list
tools/call (TextContent)
tools/call (with arguments)
resources/list
resources/read
prompts/list
Content Type Verification Returns mcp.types.TextContent (not dict)
Unit Tests test_streamablehttp_transport.py (76 tests)
TestAnyUrlSerialization (5 tests)
Doctests (22 tests)

Testing Methodology

  1. REST API Tests: Verified gateway health and CRUD operations via HTTP endpoints
  2. MCP Protocol Tests: Used mcp.ClientSession with streamablehttp_client to test full MCP protocol flow against the virtual server endpoint (/servers/{id}/mcp)
  3. Content Type Verification: Confirmed tool results return proper MCP SDK types (mcp.types.TextContent) instead of raw dicts
  4. Unit Tests:
    • 76 tests covering call_tool content type handling
    • 5 tests for AnyUrl serialization (mode="json")
    • 2 tests using actual gateway model types (mcpgateway.common.models.Annotations)
    • 22 doctests

Commits in This PR

  1. 30071fe32 - Return MCP SDK types instead of dicts
  2. 57d772800 - Preserve metadata (annotations, _meta, size)
  3. c7376f2b1 - Convert gateway Annotations to dict for MCP SDK compatibility
  4. e84178119 - Add AnyUrl serialization tests
  5. 729ff0288 - Add docstrings for interrogate coverage

Status: Ready for merge 🚀

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai crivetimihai merged commit 7a34d90 into main Jan 27, 2026
53 checks passed
@crivetimihai crivetimihai deleted the 2512_toolcall_result_validation branch January 27, 2026 15:21
hughhennelly pushed a commit to hughhennelly/mcp-context-forge that referenced this pull request Feb 8, 2026
…BM#2517)

* fix(transport): support mixed content types from MCP server tool call response

Closes IBM#2512

This fix addresses tool invocation failures for tools that return complex
content types (like ResourceLink, ImageContent, AudioContent) or contain
Pydantic-specific types like AnyUrl.

Root causes fixed:
1. tool_service.py: Usage of model_dump() without mode='json' preserved
   pydantic.AnyUrl objects, violating internal model's str type constraints.
2. streamablehttp_transport.py: Code blindly assumed types.TextContent,
   accessing .text on every item, which crashed for ResourceLink or ImageContent.

Changes:
- Updated tool_service.py to use model_dump(by_alias=True, mode='json'),
  forcing conversion of AnyUrl to JSON-compatible strings.
- Refactored streamablehttp_transport.py to inspect content.type and correctly
  map to proper MCP SDK types (TextContent, ImageContent, AudioContent,
  ResourceLink, EmbeddedResource) ensuring full protocol compatibility.
- Updated return type annotation to include all MCP content types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(transport): preserve metadata in mixed content type conversion

Addresses dropped metadata fields identified in PR IBM#2517 review:
- Preserve annotations and _meta for TextContent, ImageContent, AudioContent
- Preserve size and _meta for ResourceLink (critical for file metadata)
- Handle EmbeddedResource via model_validate

Add comprehensive regression tests for:
- Mixed content types (text, image, audio, resource_link, embedded)
- Metadata preservation (annotations, _meta, size)
- Unknown content type fallback
- Missing optional metadata handling

Closes IBM#2512

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(transport): convert gateway Annotations to dict for MCP SDK compatibility

mcpgateway.common.models.Annotations is a different Pydantic class from
mcp.types.Annotations. Passing gateway Annotations directly to MCP SDK
types causes ValidationError at runtime when real MCP responses include
annotations.

Fix:
- Add _convert_annotations() helper to convert gateway Annotations to dict
- Add _convert_meta() helper for consistent meta handling
- Apply conversion to all content types (text, image, audio, resource_link)

Add regression tests using actual gateway model types:
- test_call_tool_with_gateway_model_annotations
- test_call_tool_with_gateway_model_image_annotations

These tests use mcpgateway.common.models.TextContent/ImageContent with
mcpgateway.common.models.Annotations to verify the conversion works.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test(tool_service): add AnyUrl serialization tests for mode='json' fix

Add explicit tests for the AnyUrl serialization fix (Issue IBM#2512 root cause):
- test_anyurl_serialization_without_mode_json - demonstrates the problem
- test_anyurl_serialization_with_mode_json - verifies the fix
- test_resource_link_anyurl_serialization - ResourceLink uri field
- test_tool_result_with_resource_link_serialization - ToolResult with ResourceLink
- test_mixed_content_with_anyurl_serialization - mixed content types

These tests verify that mode='json' in model_dump() correctly serializes
AnyUrl objects to strings, preventing validation errors when content is
passed to MCP SDK types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* docs(transport): add docstrings to _convert_annotations and _convert_meta

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* docs(transport): add Args/Returns to helper function docstrings

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: hughhennnelly <hughhennelly06@gmail.com>
kcostell06 pushed a commit to kcostell06/mcp-context-forge that referenced this pull request Feb 24, 2026
…BM#2517)

* fix(transport): support mixed content types from MCP server tool call response

Closes IBM#2512

This fix addresses tool invocation failures for tools that return complex
content types (like ResourceLink, ImageContent, AudioContent) or contain
Pydantic-specific types like AnyUrl.

Root causes fixed:
1. tool_service.py: Usage of model_dump() without mode='json' preserved
   pydantic.AnyUrl objects, violating internal model's str type constraints.
2. streamablehttp_transport.py: Code blindly assumed types.TextContent,
   accessing .text on every item, which crashed for ResourceLink or ImageContent.

Changes:
- Updated tool_service.py to use model_dump(by_alias=True, mode='json'),
  forcing conversion of AnyUrl to JSON-compatible strings.
- Refactored streamablehttp_transport.py to inspect content.type and correctly
  map to proper MCP SDK types (TextContent, ImageContent, AudioContent,
  ResourceLink, EmbeddedResource) ensuring full protocol compatibility.
- Updated return type annotation to include all MCP content types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(transport): preserve metadata in mixed content type conversion

Addresses dropped metadata fields identified in PR IBM#2517 review:
- Preserve annotations and _meta for TextContent, ImageContent, AudioContent
- Preserve size and _meta for ResourceLink (critical for file metadata)
- Handle EmbeddedResource via model_validate

Add comprehensive regression tests for:
- Mixed content types (text, image, audio, resource_link, embedded)
- Metadata preservation (annotations, _meta, size)
- Unknown content type fallback
- Missing optional metadata handling

Closes IBM#2512

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix(transport): convert gateway Annotations to dict for MCP SDK compatibility

mcpgateway.common.models.Annotations is a different Pydantic class from
mcp.types.Annotations. Passing gateway Annotations directly to MCP SDK
types causes ValidationError at runtime when real MCP responses include
annotations.

Fix:
- Add _convert_annotations() helper to convert gateway Annotations to dict
- Add _convert_meta() helper for consistent meta handling
- Apply conversion to all content types (text, image, audio, resource_link)

Add regression tests using actual gateway model types:
- test_call_tool_with_gateway_model_annotations
- test_call_tool_with_gateway_model_image_annotations

These tests use mcpgateway.common.models.TextContent/ImageContent with
mcpgateway.common.models.Annotations to verify the conversion works.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test(tool_service): add AnyUrl serialization tests for mode='json' fix

Add explicit tests for the AnyUrl serialization fix (Issue IBM#2512 root cause):
- test_anyurl_serialization_without_mode_json - demonstrates the problem
- test_anyurl_serialization_with_mode_json - verifies the fix
- test_resource_link_anyurl_serialization - ResourceLink uri field
- test_tool_result_with_resource_link_serialization - ToolResult with ResourceLink
- test_mixed_content_with_anyurl_serialization - mixed content types

These tests verify that mode='json' in model_dump() correctly serializes
AnyUrl objects to strings, preventing validation errors when content is
passed to MCP SDK types.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* docs(transport): add docstrings to _convert_annotations and _convert_meta

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* docs(transport): add Args/Returns to helper function docstrings

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
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.

[BUG]: Tool invocation fails with Pydantic validation errors

2 participants