Skip to content

Commit 310ffa8

Browse files
CopilotpontemontiCopilot
authored
Add support for custom MCP server URLs (#96)
* Initial plan * Add support for custom MCP server URLs Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add comprehensive tests for custom MCP server URL support Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Remove unnecessary hasattr check in exception handler Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Simplify custom URL checks by removing redundant strip calls Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Improve exception handling and clarify test comments Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Refactor to consistently store URLs in the url field Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Use 'url' field consistently in both manifest and gateway responses Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Refactor gateway parsing to match manifest logic and improve variable naming Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Fix formatting: remove extra blank line in test Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Rename custom_url to endpoint for better clarity Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Remove fallback logic and use config.url directly in all tool registration services Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Remove redundant null check for server_url in Agent Framework service Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Use 'or' operator for cleaner fallback logic in server_name assignment Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add clarifying comment for server_name fallback logic Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Fix undefined variable reference in log statement Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Move server_name assignment outside try block to ensure it's always available in exception handler Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Clarify comment for server_name fallback logic Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add server_name fallback logic in OpenAI MCP tool registration service Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Improve variable naming consistency and add server_name fallback in Semantic Kernel service Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Fix inconsistent server_name usage in Semantic Kernel service Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Add server_name fallback logic in configuration service URL construction Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Run ruff formatter to fix formatting issues Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> * Update tests/tooling/test_mcp_server_configuration.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/tooling/test_mcp_server_configuration.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/tooling/test_mcp_server_configuration.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com> Co-authored-by: Johan Broberg <johan@pontemonti.net> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 25f7c6d commit 310ffa8

File tree

8 files changed

+322
-28
lines changed

8 files changed

+322
-28
lines changed

libraries/microsoft-agents-a365-tooling-extensions-agentframework/microsoft_agents_a365/tooling/extensions/agentframework/services/mcp_tool_registration_service.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,10 @@ async def add_tool_servers_to_agent(
9797

9898
# Add servers as MCPStreamableHTTPTool instances
9999
for config in server_configs:
100-
try:
101-
server_url = getattr(config, "server_url", None) or getattr(
102-
config, "mcp_server_unique_name", None
103-
)
104-
if not server_url:
105-
self._logger.warning(f"MCP server config missing server_url: {config}")
106-
continue
100+
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
101+
server_name = config.mcp_server_name or config.mcp_server_unique_name
107102

103+
try:
108104
# Prepare auth headers
109105
headers = {}
110106
if auth_token:
@@ -116,26 +112,23 @@ async def add_tool_servers_to_agent(
116112
self._orchestrator_name
117113
)
118114

119-
server_name = getattr(config, "mcp_server_name", "Unknown")
120-
121115
# Create and configure MCPStreamableHTTPTool
122116
mcp_tools = MCPStreamableHTTPTool(
123117
name=server_name,
124-
url=server_url,
118+
url=config.url,
125119
headers=headers,
126120
description=f"MCP tools from {server_name}",
127121
)
128122

129123
# Let Agent Framework handle the connection automatically
130-
self._logger.info(f"Created MCP plugin for '{server_name}' at {server_url}")
124+
self._logger.info(f"Created MCP plugin for '{server_name}' at {config.url}")
131125

132126
all_tools.append(mcp_tools)
133127
self._connected_servers.append(mcp_tools)
134128

135129
self._logger.info(f"Added MCP plugin '{server_name}' to agent tools")
136130

137131
except Exception as tool_ex:
138-
server_name = getattr(config, "mcp_server_name", "Unknown")
139132
self._logger.warning(
140133
f"Failed to create MCP plugin for {server_name}: {tool_ex}"
141134
)

libraries/microsoft-agents-a365-tooling-extensions-azureaifoundry/microsoft_agents_a365/tooling/extensions/azureaifoundry/services/mcp_tool_registration_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,11 @@ async def _get_mcp_tool_definitions_and_resources(
179179
else server.mcp_server_name
180180
)
181181

182+
# Use the URL from server (always populated by the configuration service)
183+
server_url = server.url
184+
182185
# Create MCP tool using Azure Foundry SDK
183-
mcp_tool = McpTool(server_label=server_label, server_url=server.mcp_server_unique_name)
186+
mcp_tool = McpTool(server_label=server_label, server_url=server_url)
184187

185188
# Configure the tool
186189
mcp_tool.set_approval_mode("never")

libraries/microsoft-agents-a365-tooling-extensions-openai/microsoft_agents_a365/tooling/extensions/openai/mcp_tool_registration_service.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,13 @@ async def add_tool_servers_to_agent(
102102
# Convert MCP server configs to MCPServerInfo objects
103103
mcp_servers_info = []
104104
for server_config in mcp_server_configs:
105+
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
106+
server_name = server_config.mcp_server_name or server_config.mcp_server_unique_name
107+
# Use the URL from config (always populated by the configuration service)
108+
server_url = server_config.url
105109
server_info = MCPServerInfo(
106-
name=server_config.mcp_server_name,
107-
url=server_config.mcp_server_unique_name,
110+
name=server_name,
111+
url=server_url,
108112
)
109113
mcp_servers_info.append(server_info)
110114

libraries/microsoft-agents-a365-tooling-extensions-semantickernel/microsoft_agents_a365/tooling/extensions/semantickernel/services/mcp_tool_registration_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,23 @@ async def add_tool_servers_to_agent(
126126
self._orchestrator_name
127127
)
128128

129+
# Use the URL from server (always populated by the configuration service)
130+
server_url = server.url
131+
132+
# Use mcp_server_name if available (not None or empty), otherwise fall back to mcp_server_unique_name
133+
server_name = server.mcp_server_name or server.mcp_server_unique_name
134+
129135
plugin = MCPStreamableHttpPlugin(
130-
name=server.mcp_server_name,
131-
url=server.mcp_server_unique_name,
136+
name=server_name,
137+
url=server_url,
132138
headers=headers,
133139
)
134140

135141
# Connect the plugin
136142
await plugin.connect()
137143

138144
# Add plugin to kernel
139-
kernel.add_plugin(plugin, server.mcp_server_name)
145+
kernel.add_plugin(plugin, server_name)
140146

141147
# Store reference to keep plugin alive throughout application lifecycle
142148
# By storing plugin references in _connected_plugins, we prevent Python's garbage collector from cleaning up the plugin objects

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/models/mcp_server_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
from dataclasses import dataclass
9+
from typing import Optional
910

1011

1112
@dataclass
@@ -20,6 +21,10 @@ class MCPServerConfig:
2021
#: Gets or sets the unique name of the MCP server.
2122
mcp_server_unique_name: str
2223

24+
#: Gets or sets the custom URL for the MCP server. If provided, this URL will be used
25+
#: instead of constructing the URL from the base URL and unique name.
26+
url: Optional[str] = None
27+
2328
def __post_init__(self):
2429
"""Validate the configuration after initialization."""
2530
if not self.mcp_server_name:

libraries/microsoft-agents-a365-tooling/microsoft_agents_a365/tooling/services/mcp_tool_server_configuration_service.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,26 @@ def _parse_manifest_server_config(
412412
MCPServerConfig object or None if parsing fails.
413413
"""
414414
try:
415-
name = self._extract_server_name(server_element)
416-
server_name = self._extract_server_unique_name(server_element)
415+
mcp_server_name = self._extract_server_name(server_element)
416+
mcp_server_unique_name = self._extract_server_unique_name(server_element)
417417

418-
if not self._validate_server_strings(name, server_name):
418+
if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name):
419419
return None
420420

421-
# Construct full URL using environment utilities
422-
full_url = build_mcp_server_url(server_name)
421+
# Check if a URL is provided
422+
endpoint = self._extract_server_url(server_element)
423423

424-
return MCPServerConfig(mcp_server_name=name, mcp_server_unique_name=full_url)
424+
# Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction
425+
server_name = mcp_server_name or mcp_server_unique_name
426+
427+
# Determine the final URL: use custom URL if provided, otherwise construct it
428+
final_url = endpoint if endpoint else build_mcp_server_url(server_name)
429+
430+
return MCPServerConfig(
431+
mcp_server_name=mcp_server_name,
432+
mcp_server_unique_name=mcp_server_unique_name,
433+
url=final_url,
434+
)
425435

426436
except Exception:
427437
return None
@@ -439,13 +449,26 @@ def _parse_gateway_server_config(
439449
MCPServerConfig object or None if parsing fails.
440450
"""
441451
try:
442-
name = self._extract_server_name(server_element)
443-
endpoint = self._extract_server_unique_name(server_element)
452+
mcp_server_name = self._extract_server_name(server_element)
453+
mcp_server_unique_name = self._extract_server_unique_name(server_element)
444454

445-
if not self._validate_server_strings(name, endpoint):
455+
if not self._validate_server_strings(mcp_server_name, mcp_server_unique_name):
446456
return None
447457

448-
return MCPServerConfig(mcp_server_name=name, mcp_server_unique_name=endpoint)
458+
# Check if a URL is provided by the gateway
459+
endpoint = self._extract_server_url(server_element)
460+
461+
# Use mcp_server_name if available, otherwise fall back to mcp_server_unique_name for URL construction
462+
server_name = mcp_server_name or mcp_server_unique_name
463+
464+
# Determine the final URL: use custom URL if provided, otherwise construct it
465+
final_url = endpoint if endpoint else build_mcp_server_url(server_name)
466+
467+
return MCPServerConfig(
468+
mcp_server_name=mcp_server_name,
469+
mcp_server_unique_name=mcp_server_unique_name,
470+
url=final_url,
471+
)
449472

450473
except Exception:
451474
return None
@@ -500,6 +523,21 @@ def _extract_server_unique_name(self, server_element: Dict[str, Any]) -> Optiona
500523
return server_element["mcpServerUniqueName"]
501524
return None
502525

526+
def _extract_server_url(self, server_element: Dict[str, Any]) -> Optional[str]:
527+
"""
528+
Extracts custom server URL from configuration element.
529+
530+
Args:
531+
server_element: Configuration dictionary.
532+
533+
Returns:
534+
Server URL string or None.
535+
"""
536+
# Check for 'url' field in both manifest and gateway responses
537+
if "url" in server_element and isinstance(server_element["url"], str):
538+
return server_element["url"]
539+
return None
540+
503541
def _validate_server_strings(self, name: Optional[str], unique_name: Optional[str]) -> bool:
504542
"""
505543
Validates that server name and unique name are valid strings.

tests/tooling/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
3+
4+
"""Tests for tooling components."""

0 commit comments

Comments
 (0)