fix(mcp): resolve SDK2 wire-shape regressions in guardrail, arize, and benchmark paths

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
joshua 2026-09-19 00:36:03 +00:00
parent 7410d00d2e
commit 4a7d8bbffa
4 changed files with 31 additions and 7 deletions

View file

@ -69,7 +69,7 @@ jobs:
uv run --frozen --no-default-groups
--with pytest==8.3.5
--with pytest-codspeed==4.3.0
--with "mcp>=1.26.0,<2.0"
--with "mcp>=2.2.0,<3.0"
--with "a2a-sdk>=1.1.0,<2.0"
pytest
-p pytest_codspeed.plugin
@ -86,7 +86,7 @@ jobs:
uv run --frozen --no-default-groups
--with pytest==8.3.5
--with pytest-codspeed==4.3.0
--with "mcp>=1.26.0,<2.0"
--with "mcp>=2.2.0,<3.0"
--with "a2a-sdk>=1.1.0,<2.0"
pytest
-p pytest_codspeed.plugin

View file

@ -1139,7 +1139,10 @@ def _set_mcp_tool_output(span: "Span", coerced_response_obj: object) -> None:
safe_set_attribute(span, SpanAttributes.OUTPUT_MIME_TYPE, OpenInferenceMimeTypeValues.TEXT.value)
return
structured: Final[object] = coerced_response_obj.get("structuredContent")
structured: Final[object] = coerced_response_obj.get(
"structured_content",
coerced_response_obj.get("structuredContent"), # pyright: ignore[reportUnknownMemberType] # tolerant dual-spelling lookup on untyped payloads
)
payload: Final[object] = content if content else structured if structured is not None else content
if payload is None:
return

View file

@ -7,7 +7,7 @@ while preserving the existing public import path.
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Final, Optional
from typing import TYPE_CHECKING, Final, Optional, cast
from fastapi import HTTPException
@ -45,6 +45,24 @@ def _serialize_mcp_content_item(item: object) -> dict[str, object]:
return {"type": "text", "text": str(item)}
def _coerce_pair_list_source(source: object) -> object:
if not isinstance(source, list):
return source
try:
return dict(cast("Sequence[tuple[str, object]]", source)) # pyright: ignore[reportUnknownArgumentType] # response_obj arrives untyped; dict() rejects non-pair shapes
except (TypeError, ValueError):
return source
def _source_field(source: object, key: str, snake_key: str) -> object:
if isinstance(source, dict):
for candidate in (key, snake_key):
if candidate in source:
return source[candidate] # pyright: ignore[reportUnknownVariableType] # dict-shaped sources arrive untyped
return None
return getattr(source, snake_key, None)
class _CiscoAIDefenseMcpMixin:
"""MCP-specific instance methods for ``CiscoAIDefenseGuardrail``.
@ -508,9 +526,10 @@ class _CiscoAIDefenseMcpMixin:
content: Sequence[object],
source: object = None,
) -> dict[str, object]:
source_map: Final[object] = _coerce_pair_list_source(source)
result: Final[dict[str, object]] = {"content": [_serialize_mcp_content_item(item) for item in content]}
for key, snake_key in (("structuredContent", "structured_content"), ("isError", "is_error")):
value = source.get(key) if isinstance(source, dict) else getattr(source, snake_key, None)
value = _source_field(source_map, key, snake_key)
if value is not None and (key != "isError" or isinstance(value, bool)):
result[key] = value
return result
@ -551,7 +570,7 @@ class _CiscoAIDefenseMcpMixin:
and all(isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], str) for item in response_obj)
):
for index, item in enumerate(response_obj):
if item[0] == "structuredContent":
if item[0] in ("structuredContent", "structured_content"):
response_obj[index] = (item[0], replacement)
replaced = True
elif hasattr(response_obj, "structured_content"):

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import enum
import re
from collections.abc import Awaitable, Callable, Mapping
@ -6,13 +8,13 @@ from typing import TYPE_CHECKING, Any, Final, Literal
from urllib.parse import urlsplit
import httpx
import httpx2
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import TypedDict
from litellm.types.llms.base import HiddenParams
if TYPE_CHECKING:
import httpx2
from mcp.types import EmbeddedResource as MCPEmbeddedResource
from mcp.types import ImageContent as MCPImageContent
from mcp.types import TextContent as MCPTextContent