Skip to content

Commit efb14ce

Browse files
eavanvalkenburgCopilotCopilot
authored
Python: Support structuredContent in MCP tool results and fix sampling options type (#4763)
* Support MCP sampling tools capability (#4625) Forward systemPrompt, tools, and toolChoice from MCP sampling requests to the chat client's get_response() call. Also advertise the sampling.tools capability to MCP servers when a client is configured. - Pass SamplingCapability with tools support to ClientSession - Convert systemPrompt to instructions in options - Convert MCP Tool objects to FunctionTool instances for options - Map MCP ToolChoice.mode to tool_choice in options - Add tests for all new behaviors and update existing sampling tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix #4625: Support MCP sampling tool with proper typing and structured content - Fix mypy error by typing sampling callback options as ChatOptions[None] instead of dict[str, Any], and importing ChatOptions from _types - Handle structuredContent from CallToolResult in _parse_tool_result_from_mcp, serializing it as JSON text Content when present - Add tests for structuredContent parsing (with and without regular content) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lint: add author to TODO comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #4625: remove default=str, add edge-case tests - Remove default=str from json.dumps for structuredContent to fail fast on non-JSON-serializable values instead of silently converting - Add test for non-JSON-serializable structuredContent (TypeError) - Add tests for empty systemPrompt ('') and empty tools list ([]) edge cases in sampling callback - Expand TODO comment noting list[Content] return type constraint for future result_type support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Sanitize sampling callback error to avoid leaking internals (#4625) Log exception details at DEBUG level instead of including them in the ErrorData message returned to the MCP server, which may be untrusted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #4625: move params to options, restore error info - Remove stale TODO comment about response_format (ChatOptions already has it) - Restore {ex} in sampling callback error message for useful debugging info - Set structuredContent as additional_property on Content for structured access - Move temperature, max_tokens, stop into options dict (not top-level kwargs) - Only set temperature when provided (not all models support it) - Add tests for generation params in options and temperature omission Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix MCP sampling callback and structured content error handling (#4625) - Guard max_tokens like temperature: only set when not None, so options can properly evaluate to None when all params are absent - Wrap json.dumps of structuredContent in try/except to fall back to str() for non-serializable values instead of propagating TypeError - Extract test_connect_sampling_capabilities_with_client into its own test function so pytest can discover it independently - Add test for max_tokens=None omission from options - Update structured content non-serializable test to expect fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #4625: review comment fixes * Fix MCP and Azure validation regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dd3d085 commit efb14ce

5 files changed

Lines changed: 409 additions & 7 deletions

File tree

python/packages/core/agent_framework/_mcp.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
from opentelemetry import propagate
1919

2020
from ._tools import FunctionTool
21-
from ._types import Content, Message
21+
from ._types import (
22+
ChatOptions,
23+
Content,
24+
Message,
25+
)
2226
from .exceptions import ToolException, ToolExecutionException
2327

2428
if sys.version_info >= (3, 11):
@@ -640,13 +644,20 @@ async def _connect_on_owner(self, *, reset: bool = False) -> None:
640644
raise ToolException(error_msg, inner_exception=ex) from ex
641645
try:
642646
try:
647+
from mcp import types
643648
from mcp.client.session import ClientSession as runtime_client_session
644649
except ModuleNotFoundError as ex:
645650
await self._safe_close_exit_stack()
646651
raise ToolException(
647652
"MCP support requires `mcp`. Please install `mcp`.",
648653
inner_exception=ex,
649654
) from ex
655+
656+
sampling_capabilities = None
657+
if self.client is not None:
658+
sampling_capabilities = types.SamplingCapability(
659+
tools=types.SamplingToolsCapability(),
660+
)
650661
session = await self._exit_stack.enter_async_context(
651662
runtime_client_session(
652663
read_stream=transport[0],
@@ -657,6 +668,7 @@ async def _connect_on_owner(self, *, reset: bool = False) -> None:
657668
message_handler=self.message_handler,
658669
logging_callback=self.logging_callback,
659670
sampling_callback=self.sampling_callback,
671+
sampling_capabilities=sampling_capabilities,
660672
)
661673
)
662674
except Exception as ex:
@@ -733,14 +745,35 @@ async def sampling_callback(
733745
messages: list[Message] = []
734746
for msg in params.messages:
735747
messages.append(self._parse_message_from_mcp(msg))
748+
749+
options: ChatOptions[None] = {}
750+
if params.systemPrompt is not None:
751+
options["instructions"] = params.systemPrompt
752+
if params.tools is not None:
753+
options["tools"] = [
754+
FunctionTool(
755+
name=tool.name,
756+
description=tool.description or "",
757+
input_model=tool.inputSchema,
758+
)
759+
for tool in params.tools
760+
]
761+
if params.toolChoice is not None and params.toolChoice.mode is not None:
762+
options["tool_choice"] = params.toolChoice.mode
763+
764+
if params.temperature is not None:
765+
options["temperature"] = params.temperature
766+
options["max_tokens"] = params.maxTokens
767+
if params.stopSequences is not None:
768+
options["stop"] = params.stopSequences
769+
736770
try:
737771
response = await self.client.get_response(
738772
messages,
739-
temperature=params.temperature,
740-
max_tokens=params.maxTokens,
741-
stop=params.stopSequences,
773+
options=options or None,
742774
)
743775
except Exception as ex:
776+
logger.debug("Sampling callback error: %s", ex, exc_info=True)
744777
return types.ErrorData(
745778
code=types.INTERNAL_ERROR,
746779
message=f"Failed to get chat message content: {ex}",

0 commit comments

Comments
 (0)