Skip to content

Commit 49e2085

Browse files
authored
Python: Fix azurefunctions MCP tool invocation to use correct agent (microsoft#3339)
* MCP tool fix for azurefunctions * Moving logic to check for thread id
1 parent 17134d6 commit 49e2085

4 files changed

Lines changed: 110 additions & 10 deletions

File tree

python/packages/azurefunctions/agent_framework_azurefunctions/_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ async def _handle_mcp_tool_invocation(
609609
# Create or parse session ID
610610
if thread_id and isinstance(thread_id, str) and thread_id.strip():
611611
try:
612-
session_id = AgentSessionId.parse(thread_id)
612+
session_id = AgentSessionId.parse(thread_id, agent_name=agent_name)
613613
except ValueError as e:
614614
logger.warning(
615615
"Failed to parse AgentSessionId from thread_id '%s': %s. Falling back to new session ID.",

python/packages/azurefunctions/agent_framework_azurefunctions/_models.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,34 @@ def __repr__(self) -> str:
109109
return f"AgentSessionId(name='{self.name}', key='{self.key}')"
110110

111111
@staticmethod
112-
def parse(session_id_string: str) -> AgentSessionId:
112+
def parse(session_id_string: str, agent_name: str | None = None) -> AgentSessionId:
113113
"""Parses a string representation of an agent session ID.
114114
115115
Args:
116-
session_id_string: A string in the form @name@key
116+
session_id_string: A string in the form @name@key, or a plain key string
117+
when agent_name is provided.
118+
agent_name: Optional agent name to use instead of parsing from the string.
119+
If provided, only the key portion is extracted from session_id_string
120+
(for @name@key format) or the entire string is used as the key
121+
(for plain strings).
117122
118123
Returns:
119124
AgentSessionId instance
120125
121126
Raises:
122-
ValueError: If the string format is invalid
127+
ValueError: If the string format is invalid and agent_name is not provided
123128
"""
124-
if not session_id_string.startswith("@"):
125-
raise ValueError(f"Invalid agent session ID format: {session_id_string}")
129+
# Check if string is in @name@key format
130+
if session_id_string.startswith("@") and "@" in session_id_string[1:]:
131+
parts = session_id_string[1:].split("@", 1)
132+
name = agent_name if agent_name is not None else parts[0]
133+
return AgentSessionId(name=name, key=parts[1])
126134

127-
parts = session_id_string[1:].split("@", 1)
128-
if len(parts) != 2:
129-
raise ValueError(f"Invalid agent session ID format: {session_id_string}")
135+
# Plain string format - only valid when agent_name is provided
136+
if agent_name is not None:
137+
return AgentSessionId(name=agent_name, key=session_id_string)
130138

131-
return AgentSessionId(name=parts[0], key=parts[1])
139+
raise ValueError(f"Invalid agent session ID format: {session_id_string}")
132140

133141

134142
class DurableAgentThread(AgentThread):

python/packages/azurefunctions/tests/test_app.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,70 @@ async def test_handle_mcp_tool_invocation_runtime_error(self) -> None:
10561056
with pytest.raises(RuntimeError, match="Agent execution failed"):
10571057
await app._handle_mcp_tool_invocation("TestAgent", context, client)
10581058

1059+
async def test_handle_mcp_tool_invocation_ignores_agent_name_in_thread_id(self) -> None:
1060+
"""Test that MCP tool invocation uses the agent_name parameter, not the name from thread_id."""
1061+
mock_agent = Mock()
1062+
mock_agent.name = "PlantAdvisor"
1063+
1064+
app = AgentFunctionApp(agents=[mock_agent])
1065+
client = AsyncMock()
1066+
1067+
# Mock the entity response
1068+
mock_state = Mock()
1069+
mock_state.entity_state = {
1070+
"schemaVersion": "1.0.0",
1071+
"data": {"conversationHistory": []},
1072+
}
1073+
client.read_entity_state.return_value = mock_state
1074+
1075+
# Thread ID contains a different agent name (@StockAdvisor@poc123)
1076+
# but we're invoking PlantAdvisor - it should use PlantAdvisor's entity
1077+
context = json.dumps({"arguments": {"query": "test query", "threadId": "@StockAdvisor@test123"}})
1078+
1079+
with patch.object(app, "_get_response_from_entity") as get_response_mock:
1080+
get_response_mock.return_value = {"status": "success", "response": "Test response"}
1081+
1082+
await app._handle_mcp_tool_invocation("PlantAdvisor", context, client)
1083+
1084+
# Verify signal_entity was called with PlantAdvisor's entity, not StockAdvisor's
1085+
client.signal_entity.assert_called_once()
1086+
call_args = client.signal_entity.call_args
1087+
entity_id = call_args[0][0]
1088+
1089+
# Entity name should be dafx-PlantAdvisor, not dafx-StockAdvisor
1090+
assert entity_id.name == "dafx-PlantAdvisor"
1091+
assert entity_id.key == "test123"
1092+
1093+
async def test_handle_mcp_tool_invocation_uses_plain_thread_id_as_key(self) -> None:
1094+
"""Test that a plain thread_id (not in @name@key format) is used as-is for the key."""
1095+
mock_agent = Mock()
1096+
mock_agent.name = "TestAgent"
1097+
1098+
app = AgentFunctionApp(agents=[mock_agent])
1099+
client = AsyncMock()
1100+
1101+
mock_state = Mock()
1102+
mock_state.entity_state = {
1103+
"schemaVersion": "1.0.0",
1104+
"data": {"conversationHistory": []},
1105+
}
1106+
client.read_entity_state.return_value = mock_state
1107+
1108+
# Plain thread_id without @name@key format
1109+
context = json.dumps({"arguments": {"query": "test query", "threadId": "simple-thread-123"}})
1110+
1111+
with patch.object(app, "_get_response_from_entity") as get_response_mock:
1112+
get_response_mock.return_value = {"status": "success", "response": "Test response"}
1113+
1114+
await app._handle_mcp_tool_invocation("TestAgent", context, client)
1115+
1116+
client.signal_entity.assert_called_once()
1117+
call_args = client.signal_entity.call_args
1118+
entity_id = call_args[0][0]
1119+
1120+
assert entity_id.name == "dafx-TestAgent"
1121+
assert entity_id.key == "simple-thread-123"
1122+
10591123
def test_health_check_includes_mcp_tool_enabled(self) -> None:
10601124
"""Test that health check endpoint includes mcp_tool_enabled field."""
10611125
mock_agent = Mock()

python/packages/azurefunctions/tests/test_models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,34 @@ def test_parse_round_trip(self) -> None:
120120
assert parsed.name == original.name
121121
assert parsed.key == original.key
122122

123+
def test_parse_with_agent_name_override(self) -> None:
124+
"""Test parsing @name@key format with agent_name parameter overrides the name."""
125+
session_id = AgentSessionId.parse("@OriginalAgent@test-key-123", agent_name="OverriddenAgent")
126+
127+
assert session_id.name == "OverriddenAgent"
128+
assert session_id.key == "test-key-123"
129+
130+
def test_parse_without_agent_name_uses_parsed_name(self) -> None:
131+
"""Test parsing @name@key format without agent_name uses name from string."""
132+
session_id = AgentSessionId.parse("@ParsedAgent@test-key-123")
133+
134+
assert session_id.name == "ParsedAgent"
135+
assert session_id.key == "test-key-123"
136+
137+
def test_parse_plain_string_with_agent_name(self) -> None:
138+
"""Test parsing plain string with agent_name uses entire string as key."""
139+
session_id = AgentSessionId.parse("simple-thread-123", agent_name="TestAgent")
140+
141+
assert session_id.name == "TestAgent"
142+
assert session_id.key == "simple-thread-123"
143+
144+
def test_parse_plain_string_without_agent_name_raises(self) -> None:
145+
"""Test parsing plain string without agent_name raises ValueError."""
146+
with pytest.raises(ValueError) as exc_info:
147+
AgentSessionId.parse("simple-thread-123")
148+
149+
assert "Invalid agent session ID format" in str(exc_info.value)
150+
123151
def test_to_entity_name_adds_prefix(self) -> None:
124152
"""Test that to_entity_name adds the dafx- prefix."""
125153
entity_name = AgentSessionId.to_entity_name("TestAgent")

0 commit comments

Comments
 (0)