From 1dc0a0f744666273c693473d0818eb0a8462019c Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Sun, 31 Aug 2025 20:33:20 +0300 Subject: [PATCH 01/13] Fix plugin logs not showing and mcp register tools --- .../utcp_client_implementation.py | 12 ++- .../utcp_cli/cli_communication_protocol.py | 7 ++ .../utcp_gql/gql_communication_protocol.py | 8 ++ .../utcp_http/http_communication_protocol.py | 7 ++ .../utcp_http/sse_communication_protocol.py | 7 ++ .../streamable_http_communication_protocol.py | 7 ++ .../utcp_mcp/mcp_communication_protocol.py | 88 ++++++++++++------- .../utcp_socket/tcp_communication_protocol.py | 7 ++ .../utcp_text/text_communication_protocol.py | 8 ++ 9 files changed, 116 insertions(+), 35 deletions(-) diff --git a/core/src/utcp/implementations/utcp_client_implementation.py b/core/src/utcp/implementations/utcp_client_implementation.py index 555ba2e..53e6b71 100644 --- a/core/src/utcp/implementations/utcp_client_implementation.py +++ b/core/src/utcp/implementations/utcp_client_implementation.py @@ -110,11 +110,21 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM raise ValueError(f"No registered communication protocol of type {manual_call_template.call_template_type} found, available types: {CommunicationProtocol.communication_protocols.keys()}") result = await CommunicationProtocol.communication_protocols[manual_call_template.call_template_type].register_manual(self, manual_call_template) - + if result.success: + final_tools = [] for tool in result.manual.tools: if not tool.name.startswith(manual_call_template.name + "."): tool.name = manual_call_template.name + "." + tool.name + + if tool.tool_call_template.call_template_type != "mcp": + final_tools.append(tool) + else: + mcp_result = await CommunicationProtocol.communication_protocols["mcp"].register_manual(self, tool.tool_call_template) + if mcp_result.success: + final_tools.extend(mcp_result.manual.tools) + + result.manual.tools = final_tools await self.config.tool_repository.save_manual(result.manual_call_template, result.manual) return result diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py index 148e261..cc5d172 100644 --- a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py +++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py @@ -23,6 +23,7 @@ import json import os import shlex +import sys from typing import Dict, Any, List, Optional, Callable, AsyncGenerator from utcp.interfaces.communication_protocol import CommunicationProtocol @@ -35,6 +36,12 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + class CliCommunicationProtocol(CommunicationProtocol): """REQUIRED diff --git a/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py b/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py index 523a97c..771aad4 100644 --- a/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py +++ b/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py @@ -1,3 +1,4 @@ +import sys from typing import Dict, Any, List, Optional, Callable import aiohttp import asyncio @@ -12,6 +13,13 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + class GraphQLClientTransport(ClientTransportInterface): """ Simple, robust, production-ready GraphQL transport using gql. diff --git a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py index 68de65e..9be24fc 100644 --- a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py @@ -12,6 +12,7 @@ - Request/response handling with proper error management """ +import sys from typing import Dict, Any, List, Optional, Callable, AsyncGenerator import aiohttp import json @@ -36,6 +37,12 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + class HttpCommunicationProtocol(CommunicationProtocol): """REQUIRED HTTP communication protocol implementation for UTCP client. diff --git a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py index b3fa8d5..e4272a0 100644 --- a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py @@ -1,3 +1,4 @@ +import sys from typing import Dict, Any, List, Optional, Callable, AsyncIterator, AsyncGenerator import aiohttp import json @@ -21,6 +22,12 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + class SseCommunicationProtocol(CommunicationProtocol): """REQUIRED SSE communication protocol implementation for UTCP client. diff --git a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py index 947470f..df8e86c 100644 --- a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py @@ -1,3 +1,4 @@ +import sys from typing import Dict, Any, List, Optional, Callable, AsyncIterator, Tuple, AsyncGenerator import aiohttp import json @@ -18,6 +19,12 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + class StreamableHttpCommunicationProtocol(CommunicationProtocol): """REQUIRED Streamable HTTP communication protocol implementation for UTCP client. diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py index 1bd238d..1ae591d 100644 --- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py +++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py @@ -1,3 +1,4 @@ +import sys from typing import Any, Dict, Optional, AsyncGenerator, TYPE_CHECKING import json @@ -17,6 +18,13 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + class McpCommunicationProtocol(CommunicationProtocol): """REQUIRED MCP transport implementation that connects to MCP servers via stdio or HTTP. @@ -28,6 +36,18 @@ class McpCommunicationProtocol(CommunicationProtocol): def __init__(self): self._oauth_tokens: Dict[str, Dict[str, Any]] = {} self._mcp_client: Optional[MCPClient] = None + + def _log_info(self, message: str): + """Log informational messages.""" + logger.info(f"[McpCommunicationProtocol] {message}") + + def _log_warning(self, message: str): + """Log informational messages.""" + logger.warning(f"[McpCommunicationProtocol] {message}") + + def _log_error(self, message: str): + """Log error messages.""" + logger.error(f"[McpCommunicationProtocol] {message}") async def _ensure_mcp_client(self, manual_call_template: 'McpCallTemplate'): """Ensure MCPClient is initialized with the current configuration.""" @@ -43,11 +63,11 @@ async def _get_or_create_session(self, server_name: str, manual_call_template: ' try: # Try to get existing session session = self._mcp_client.get_session(server_name) - logger.info(f"Reusing existing session for server: {server_name}") + self._log_info(f"Reusing existing session for server: {server_name}") return session except ValueError: # Session doesn't exist, create a new one - logger.info(f"Creating new session for server: {server_name}") + self._log_info(f"Creating new session for server: {server_name}") session = await self._mcp_client.create_session(server_name, auto_initialize=True) return session @@ -55,13 +75,13 @@ async def _cleanup_session(self, server_name: str): """Clean up a specific session.""" if self._mcp_client: await self._mcp_client.close_session(server_name) - logger.info(f"Cleaned up session for server: {server_name}") + self._log_info(f"Cleaned up session for server: {server_name}") async def _cleanup_all_sessions(self): """Clean up all active sessions.""" if self._mcp_client: await self._mcp_client.close_all_sessions() - logger.info("Cleaned up all sessions") + self._log_info("Cleaned up all sessions") async def _list_tools_with_session(self, server_name: str, manual_call_template: 'McpCallTemplate'): """List tools using cached session when possible.""" @@ -86,7 +106,7 @@ async def _list_tools_with_session(self, server_name: str, manual_call_template: if is_session_error: # Only restart session for connection/transport level issues await self._cleanup_session(server_name) - logger.warning(f"Session-level error for list_tools, retrying with fresh session: {e}") + self._log_warning(f"Session-level error for list_tools, retrying with fresh session: {e}") # Retry with a fresh session session = await self._get_or_create_session(server_name, manual_call_template) @@ -98,7 +118,7 @@ async def _list_tools_with_session(self, server_name: str, manual_call_template: return tools_response else: # Protocol-level error, re-raise without session restart - logger.error(f"Protocol-level error for list_tools: {e}") + self._log_error(f"Protocol-level error for list_tools: {e}") raise async def _list_resources_with_session(self, server_name: str, manual_call_template: 'McpCallTemplate'): @@ -114,7 +134,7 @@ async def _list_resources_with_session(self, server_name: str, manual_call_templ except Exception as e: # If there's an error, clean up the potentially bad session and try once more await self._cleanup_session(server_name) - logger.warning(f"Session failed for list_resources, retrying: {e}") + self._log_warning(f"Session failed for list_resources, retrying: {e}") # Retry with a fresh session session = await self._get_or_create_session(server_name, manual_call_template) @@ -134,7 +154,7 @@ async def _read_resource_with_session(self, server_name: str, manual_call_templa except Exception as e: # If there's an error, clean up the potentially bad session and try once more await self._cleanup_session(server_name) - logger.warning(f"Session failed for read_resource '{resource_uri}', retrying: {e}") + self._log_warning(f"Session failed for read_resource '{resource_uri}', retrying: {e}") # Retry with a fresh session session = await self._get_or_create_session(server_name, manual_call_template) @@ -149,7 +169,7 @@ async def _handle_resource_call(self, resource_name: str, tool_call_template: 'M # Try each server until we find one that has the resource for server_name, server_config in tool_call_template.config.mcpServers.items(): try: - logger.info(f"Attempting to find resource '{resource_name}' on server '{server_name}'") + self._log_info(f"Attempting to find resource '{resource_name}' on server '{server_name}'") # List resources to find the one with matching name resources = await self._list_resources_with_session(server_name, tool_call_template) @@ -160,17 +180,17 @@ async def _handle_resource_call(self, resource_name: str, tool_call_template: 'M break if target_resource is None: - logger.info(f"Resource '{resource_name}' not found in server '{server_name}'") + self._log_info(f"Resource '{resource_name}' not found in server '{server_name}'") continue # Try next server # Read the resource - logger.info(f"Reading resource '{resource_name}' with URI '{target_resource.uri}' from server '{server_name}'") + self._log_info(f"Reading resource '{resource_name}' with URI '{target_resource.uri}' from server '{server_name}'") result = await self._read_resource_with_session(server_name, tool_call_template, target_resource.uri) # Process the result return result.model_dump() except Exception as e: - logger.error(f"Error reading resource '{resource_name}' on server '{server_name}': {e}") + self._log_error(f"Error reading resource '{resource_name}' on server '{server_name}': {e}") continue # Try next server raise ValueError(f"Resource '{resource_name}' not found in any configured server") @@ -192,9 +212,9 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call if manual_call_template.config and manual_call_template.config.mcpServers: for server_name, server_config in manual_call_template.config.mcpServers.items(): try: - logger.info(f"Discovering tools for server '{server_name}' via {server_config}") + self._log_info(f"Discovering tools for server '{server_name}' via {server_config}") mcp_tools = await self._list_tools_with_session(server_name, manual_call_template) - logger.info(f"Discovered {len(mcp_tools)} tools for server '{server_name}'") + self._log_info(f"Discovered {len(mcp_tools)} tools for server '{server_name}'") for mcp_tool in mcp_tools: # Convert mcp.Tool to utcp.data.tool.Tool utcp_tool = Tool( @@ -208,10 +228,10 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call # Register resources as tools if enabled if manual_call_template.register_resources_as_tools: - logger.info(f"Discovering resources for server '{server_name}' to register as tools") + self._log_info(f"Discovering resources for server '{server_name}' to register as tools") try: mcp_resources = await self._list_resources_with_session(server_name, manual_call_template) - logger.info(f"Discovered {len(mcp_resources)} resources for server '{server_name}'") + self._log_info(f"Discovered {len(mcp_resources)} resources for server '{server_name}'") for mcp_resource in mcp_resources: # Convert mcp.Resource to utcp.data.tool.Tool # Create a tool that reads the resource when called @@ -236,11 +256,11 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call ) all_tools.append(resource_tool) except Exception as resource_error: - logger.warning(f"Failed to discover resources for server '{server_name}': {resource_error}") + self._log_warning(f"Failed to discover resources for server '{server_name}': {resource_error}") # Don't add this to errors since resources are optional except Exception as e: - logger.error(f"Failed to discover tools for server '{server_name}': {e}") + self._log_error(f"Failed to discover tools for server '{server_name}': {e}") errors.append(f"Failed to discover tools for server '{server_name}': {e}") return RegisterManualResult( manual_call_template=manual_call_template, @@ -271,14 +291,14 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[ # Try each server until we find one that has the tool for server_name, server_config in tool_call_template.config.mcpServers.items(): try: - logger.info(f"Attempting to call tool '{tool_name}' on server '{server_name}'") + self._log_info(f"Attempting to call tool '{tool_name}' on server '{server_name}'") # First check if this server has the tool tools = await self._list_tools_with_session(server_name, tool_call_template) tool_names = [tool.name for tool in tools] if tool_name not in tool_names: - logger.info(f"Tool '{tool_name}' not found in server '{server_name}'") + self._log_info(f"Tool '{tool_name}' not found in server '{server_name}'") continue # Try next server # Call the tool @@ -287,7 +307,7 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[ # Process the result return self._process_tool_result(result, tool_name) except Exception as e: - logger.error(f"Error calling tool '{tool_name}' on server '{server_name}': {e}") + self._log_error(f"Error calling tool '{tool_name}' on server '{server_name}': {e}") raise e raise ValueError(f"Tool '{tool_name}' not found in any configured server") @@ -298,21 +318,21 @@ async def call_tool_streaming(self, caller: 'UtcpClient', tool_name: str, tool_a yield self.call_tool(caller, tool_name, tool_args, tool_call_template) def _process_tool_result(self, result, tool_name: str) -> Any: - logger.info(f"Processing tool result for '{tool_name}', type: {type(result)}") + self._log_info(f"Processing tool result for '{tool_name}', type: {type(result)}") # Check for structured output first if hasattr(result, 'structured_output'): - logger.info(f"Found structured_output: {result.structured_output}") + self._log_info(f"Found structured_output: {result.structured_output}") return result.structured_output # Process content if available if hasattr(result, 'content'): content = result.content - logger.info(f"Content type: {type(content)}") + self._log_info(f"Content type: {type(content)}") # Handle list content if isinstance(content, list): - logger.info(f"Content is a list with {len(content)} items") + self._log_info(f"Content is a list with {len(content)} items") if not content: return [] @@ -376,23 +396,23 @@ def _parse_text_content(self, text: str) -> Any: async def deregister_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> None: """Deregister an MCP manual and clean up associated sessions.""" if not isinstance(manual_call_template, McpCallTemplate): - logger.info(f"Deregistering manual '{manual_call_template.name}' - not an MCP template") + self._log_info(f"Deregistering manual '{manual_call_template.name}' - not an MCP template") return - logger.info(f"Deregistering manual '{manual_call_template.name}' and cleaning up sessions") + self._log_info(f"Deregistering manual '{manual_call_template.name}' and cleaning up sessions") # Clean up sessions for all servers in this manual if manual_call_template.config and manual_call_template.config.mcpServers: for server_name, server_config in manual_call_template.config.mcpServers.items(): await self._cleanup_session(server_name) - logger.info(f"Cleaned up session for server '{server_name}'") + self._log_info(f"Cleaned up session for server '{server_name}'") async def close(self) -> None: """Close all active sessions and clean up resources.""" - logger.info("Closing MCP communication protocol and cleaning up all sessions") + self._log_info("Closing MCP communication protocol and cleaning up all sessions") await self._cleanup_all_sessions() self._session_locks.clear() - logger.info("MCP communication protocol closed successfully") + self._log_info("MCP communication protocol closed successfully") async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str: """Handles OAuth2 client credentials flow, trying both body and auth header methods.""" @@ -405,7 +425,7 @@ async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str: async with aiohttp.ClientSession() as session: # Method 1: Send credentials in the request body try: - logger.info(f"Attempting OAuth2 token fetch for '{client_id}' with credentials in body.") + self._log_info(f"Attempting OAuth2 token fetch for '{client_id}' with credentials in body.") body_data = { 'grant_type': 'client_credentials', 'client_id': client_id, @@ -418,11 +438,11 @@ async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str: self._oauth_tokens[client_id] = token_response return token_response["access_token"] except aiohttp.ClientError as e: - logger.error(f"OAuth2 with credentials in body failed: {e}. Trying Basic Auth header.") + self._log_error(f"OAuth2 with credentials in body failed: {e}. Trying Basic Auth header.") # Method 2: Send credentials as Basic Auth header try: - logger.info(f"Attempting OAuth2 token fetch for '{client_id}' with Basic Auth header.") + self._log_info(f"Attempting OAuth2 token fetch for '{client_id}' with Basic Auth header.") header_auth = AiohttpBasicAuth(client_id, auth_details.client_secret) header_data = { 'grant_type': 'client_credentials', @@ -434,5 +454,5 @@ async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str: self._oauth_tokens[client_id] = token_response return token_response["access_token"] except aiohttp.ClientError as e: - logger.error(f"OAuth2 with Basic Auth header also failed: {e}") + self._log_error(f"OAuth2 with Basic Auth header also failed: {e}") raise e diff --git a/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py b/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py index 95d0e5f..9db2c13 100644 --- a/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py +++ b/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py @@ -7,6 +7,7 @@ import json import socket import struct +import sys from typing import Dict, Any, List, Optional, Callable, Union from utcp.client.client_transport_interface import ClientTransportInterface @@ -16,6 +17,12 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + class TCPTransport(ClientTransportInterface): """Transport implementation for TCP-based tool providers. diff --git a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py index 4a66b56..aaa156b 100644 --- a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py +++ b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py @@ -5,6 +5,7 @@ tools. It does not maintain any persistent connections. """ import json +import sys import yaml import aiofiles from pathlib import Path @@ -25,6 +26,13 @@ logger = logging.getLogger(__name__) +if not logger.handlers: # Only add default handler if user didn't configure logging + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + class TextCommunicationProtocol(CommunicationProtocol): """REQUIRED Communication protocol for file-based UTCP manuals and tools.""" From 5c145a39ad1a8ee8605992cb6a522b6cb5ed0b2e Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Sun, 31 Aug 2025 20:37:39 +0300 Subject: [PATCH 02/13] Add tool name --- core/src/utcp/implementations/utcp_client_implementation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/utcp/implementations/utcp_client_implementation.py b/core/src/utcp/implementations/utcp_client_implementation.py index 53e6b71..79ec753 100644 --- a/core/src/utcp/implementations/utcp_client_implementation.py +++ b/core/src/utcp/implementations/utcp_client_implementation.py @@ -122,6 +122,9 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM else: mcp_result = await CommunicationProtocol.communication_protocols["mcp"].register_manual(self, tool.tool_call_template) if mcp_result.success: + for mcp_tool in mcp_result.manual.tools: + if not mcp_tool.name.startswith(tool.name + "."): + mcp_tool.name = tool.name + "." + mcp_tool.name final_tools.extend(mcp_result.manual.tools) result.manual.tools = final_tools From 133c1878acdf1af742d9dc84d3f21c41dee59e4f Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Sun, 31 Aug 2025 21:56:58 +0300 Subject: [PATCH 03/13] Performance optimisation --- .../utcp_mcp/mcp_communication_protocol.py | 163 ++++++++++++------ 1 file changed, 109 insertions(+), 54 deletions(-) diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py index 1ae591d..4619b2d 100644 --- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py +++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Dict, Optional, AsyncGenerator, TYPE_CHECKING +from typing import Any, Dict, Optional, AsyncGenerator, TYPE_CHECKING, Tuple import json from mcp_use import MCPClient @@ -42,7 +42,7 @@ def _log_info(self, message: str): logger.info(f"[McpCommunicationProtocol] {message}") def _log_warning(self, message: str): - """Log informational messages.""" + """Log warning messages.""" logger.warning(f"[McpCommunicationProtocol] {message}") def _log_error(self, message: str): @@ -161,40 +161,6 @@ async def _read_resource_with_session(self, server_name: str, manual_call_templa result = await session.read_resource(resource_uri) return result - async def _handle_resource_call(self, resource_name: str, tool_call_template: 'McpCallTemplate') -> Any: - """Handle a resource call by finding and reading the resource from the appropriate server.""" - if not tool_call_template.config or not tool_call_template.config.mcpServers: - raise ValueError(f"No server configuration found for resource '{resource_name}'") - - # Try each server until we find one that has the resource - for server_name, server_config in tool_call_template.config.mcpServers.items(): - try: - self._log_info(f"Attempting to find resource '{resource_name}' on server '{server_name}'") - - # List resources to find the one with matching name - resources = await self._list_resources_with_session(server_name, tool_call_template) - target_resource = None - for resource in resources: - if resource.name == resource_name: - target_resource = resource - break - - if target_resource is None: - self._log_info(f"Resource '{resource_name}' not found in server '{server_name}'") - continue # Try next server - - # Read the resource - self._log_info(f"Reading resource '{resource_name}' with URI '{target_resource.uri}' from server '{server_name}'") - result = await self._read_resource_with_session(server_name, tool_call_template, target_resource.uri) - - # Process the result - return result.model_dump() - except Exception as e: - self._log_error(f"Error reading resource '{resource_name}' on server '{server_name}': {e}") - continue # Try next server - - raise ValueError(f"Resource '{resource_name}' not found in any configured server") - async def _call_tool_with_session(self, server_name: str, manual_call_template: 'McpCallTemplate', tool_name: str, inputs: Dict[str, Any]): """Call a tool using cached session when possible.""" session = await self._get_or_create_session(server_name, manual_call_template) @@ -280,28 +246,30 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[ if not tool_call_template.config or not tool_call_template.config.mcpServers: raise ValueError(f"No server configuration found for tool '{tool_name}'") - if "." in tool_name: - tool_name = tool_name.split(".", 1)[1] - - # Check if this is a resource call (tools created from resources have "resource_" prefix) - if tool_name.startswith("resource_"): - resource_name = tool_name[9:] # Remove "resource_" prefix - return await self._handle_resource_call(resource_name, tool_call_template) - - # Try each server until we find one that has the tool - for server_name, server_config in tool_call_template.config.mcpServers.items(): + parse_result = await self._parse_tool_name(tool_name, tool_call_template) + + if parse_result.is_resource: + resource_name = parse_result.name + server_name = parse_result.server_name + target_resource = parse_result.target_resource + try: - self._log_info(f"Attempting to call tool '{tool_name}' on server '{server_name}'") - - # First check if this server has the tool - tools = await self._list_tools_with_session(server_name, tool_call_template) - tool_names = [tool.name for tool in tools] + # Read the resource + self._log_info(f"Reading resource '{resource_name}' with URI '{target_resource.uri}' from server '{server_name}'") + result = await self._read_resource_with_session(server_name, tool_call_template, target_resource.uri) - if tool_name not in tool_names: - self._log_info(f"Tool '{tool_name}' not found in server '{server_name}'") - continue # Try next server + # Process the result + return result.model_dump() + except Exception as e: + self._log_error(f"Error reading resource '{resource_name}' on server '{server_name}': {e}") + raise e + else: + tool_name = parse_result.name + server_name = parse_result.server_name + try: # Call the tool + self._log_info(f"Call tool '{tool_name}' from server '{server_name}'") result = await self._call_tool_with_session(server_name, tool_call_template, tool_name, tool_args) # Process the result @@ -309,8 +277,95 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[ except Exception as e: self._log_error(f"Error calling tool '{tool_name}' on server '{server_name}': {e}") raise e + + class _ParseToolResult: + def __init__(self, manual_name: Optional[str], server_name: str, name: str, is_resource: bool, target_resource: Any): + self.manual_name = manual_name + self.server_name = server_name + self.name = name + self.is_resource = is_resource + self.target_resource = target_resource + + async def _parse_tool_name(self, tool_name: str, tool_call_template: McpCallTemplate) -> _ParseToolResult: + def normalize(val): + if isinstance(val, tuple): + return val + return (val, None) + + if "." not in tool_name: + is_resource, name = self._is_resource(tool_name) + server_name, target_resource = normalize(await self._get_tool_server(name, tool_call_template) if not is_resource else await self._get_resource_server(name, tool_call_template)) + return McpCommunicationProtocol._ParseToolResult(None, server_name, name, is_resource, target_resource) + + split = tool_name.split(".", 1) + manual_name = split[0] + tool_name = split[1] + + if "." not in tool_name: + is_resource, name = self._is_resource(tool_name) + server_name, target_resource = normalize(await self._get_tool_server(name, tool_call_template) if not is_resource else await self._get_resource_server(name, tool_call_template)) + return McpCommunicationProtocol._ParseToolResult(manual_name, server_name, name, is_resource, target_resource) + + split = tool_name.split(".", 1) + server_name = split[0] + tool_name = split[1] + + is_resource, name = self._is_resource(tool_name) + server_name, target_resource = normalize(await self._get_tool_server(name, tool_call_template) if not is_resource else await self._get_resource_server(name, tool_call_template)) + return McpCommunicationProtocol._ParseToolResult(manual_name, server_name, name, is_resource, target_resource) + + def _is_resource(self, tool_name) -> Tuple[bool, str]: + resource_prefix = "resource_" + resource_length = len(resource_prefix) + + if tool_name.startswith(resource_prefix): + return True, tool_name[resource_length:] + + return False, tool_name + + async def _get_tool_server(self, tool_name: str, tool_call_template: McpCallTemplate) -> str: + if "." in tool_name: + split = tool_name.split(".", 1) + server_name = split[0] + tool_name = split[1] + + return server_name + + # Try each server until we find one that has the tool + for server_name, server_config in tool_call_template.config.mcpServers.items(): + self._log_info(f"Attempting to call tool '{tool_name}' on server '{server_name}'") + + # First check if this server has the tool + tools = await self._list_tools_with_session(server_name, tool_call_template) + tool_names = [tool.name for tool in tools] + + if tool_name not in tool_names: + self._log_info(f"Tool '{tool_name}' not found in server '{server_name}'") + continue # Try next server + + return server_name raise ValueError(f"Tool '{tool_name}' not found in any configured server") + + async def _get_resource_server(self, resource_name: str, tool_call_template: McpCallTemplate) -> Tuple[str, Any]: + for server_name, server_config in tool_call_template.config.mcpServers.items(): + self._log_info(f"Attempting to find resource '{resource_name}' on server '{server_name}'") + + # List resources to find the one with matching name + resources = await self._list_resources_with_session(server_name, tool_call_template) + target_resource = None + for resource in resources: + if resource.name == resource_name: + target_resource = resource + break + + if target_resource is None: + self._log_info(f"Resource '{resource_name}' not found in server '{server_name}'") + continue # Try next server + + return server_name, target_resource + + raise ValueError(f"Resource '{resource_name}' not found in any configured server") async def call_tool_streaming(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]: """REQUIRED From a27941e07a5404984599338d177b856adcf3deb0 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:08:18 +0200 Subject: [PATCH 04/13] Update plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../http/src/utcp_http/http_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py index 9be24fc..15e2c23 100644 --- a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py @@ -37,7 +37,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From 68b06be663408b05651be78e76dbf86a3e2e18c1 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:11:18 +0200 Subject: [PATCH 05/13] Update plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../http/src/utcp_http/sse_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py index e4272a0..a5aac67 100644 --- a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From 33785d0559fefced673f85267bae86288886019e Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:12:27 +0200 Subject: [PATCH 06/13] Update plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../gql/src/utcp_gql/gql_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py b/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py index 771aad4..d558e82 100644 --- a/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py +++ b/plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From 677ec11186a35f87d4523f15d9e87492d154ef41 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:12:57 +0200 Subject: [PATCH 07/13] Update plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../mcp/src/utcp_mcp/mcp_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py index 4619b2d..b8fc20f 100644 --- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py +++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From 0887a006c078c49bc52544e1a5c22a305580bbb4 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:13:33 +0200 Subject: [PATCH 08/13] Update plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../cli/src/utcp_cli/cli_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py index cc5d172..abb1690 100644 --- a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py +++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From 26ba8c30db23670841748b0c39a61ce643bd9b75 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 14:14:29 +0200 Subject: [PATCH 09/13] Update plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../text/src/utcp_text/text_communication_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py index aaa156b..e35373b 100644 --- a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py +++ b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) From c685a6e63f4cce760236aa987ae740745e760734 Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Thu, 4 Sep 2025 09:42:51 +0300 Subject: [PATCH 10/13] Revert mcp specific config --- .../implementations/utcp_client_implementation.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/core/src/utcp/implementations/utcp_client_implementation.py b/core/src/utcp/implementations/utcp_client_implementation.py index 79ec753..01c13a9 100644 --- a/core/src/utcp/implementations/utcp_client_implementation.py +++ b/core/src/utcp/implementations/utcp_client_implementation.py @@ -112,22 +112,9 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM result = await CommunicationProtocol.communication_protocols[manual_call_template.call_template_type].register_manual(self, manual_call_template) if result.success: - final_tools = [] for tool in result.manual.tools: if not tool.name.startswith(manual_call_template.name + "."): tool.name = manual_call_template.name + "." + tool.name - - if tool.tool_call_template.call_template_type != "mcp": - final_tools.append(tool) - else: - mcp_result = await CommunicationProtocol.communication_protocols["mcp"].register_manual(self, tool.tool_call_template) - if mcp_result.success: - for mcp_tool in mcp_result.manual.tools: - if not mcp_tool.name.startswith(tool.name + "."): - mcp_tool.name = tool.name + "." + mcp_tool.name - final_tools.extend(mcp_result.manual.tools) - - result.manual.tools = final_tools await self.config.tool_repository.save_manual(result.manual_call_template, result.manual) return result From 7c8f0d2536331c7523ba7226ebbfb83802079e02 Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Thu, 4 Sep 2025 09:57:52 +0300 Subject: [PATCH 11/13] Add server name to mcp tool --- .../mcp/src/utcp_mcp/mcp_communication_protocol.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py index b8fc20f..de2e8c4 100644 --- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py +++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py @@ -83,6 +83,14 @@ async def _cleanup_all_sessions(self): await self._mcp_client.close_all_sessions() self._log_info("Cleaned up all sessions") + def _add_server_to_tool_name(self, tools, server_name: str): + """Prefix tool names with server name to ensure uniqueness.""" + for tool in tools: + if not tool.name.startswith(f"{server_name}."): + tool.name = f"{server_name}.{tool.name}" + + return tools + async def _list_tools_with_session(self, server_name: str, manual_call_template: 'McpCallTemplate'): """List tools using cached session when possible.""" try: @@ -180,6 +188,8 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call try: self._log_info(f"Discovering tools for server '{server_name}' via {server_config}") mcp_tools = await self._list_tools_with_session(server_name, manual_call_template) + mcp_tools = self._add_server_to_tool_name(mcp_tools, server_name) + self._log_info(f"Discovered {len(mcp_tools)} tools for server '{server_name}'") for mcp_tool in mcp_tools: # Convert mcp.Tool to utcp.data.tool.Tool @@ -202,7 +212,7 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call # Convert mcp.Resource to utcp.data.tool.Tool # Create a tool that reads the resource when called resource_tool = Tool( - name=f"resource_{mcp_resource.name}", + name=f"{server_name}.resource_{mcp_resource.name}", description=f"Read resource: {mcp_resource.description or mcp_resource.name}. URI: {mcp_resource.uri}", input_schema={ "type": "object", @@ -228,6 +238,7 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call except Exception as e: self._log_error(f"Failed to discover tools for server '{server_name}': {e}") errors.append(f"Failed to discover tools for server '{server_name}': {e}") + return RegisterManualResult( manual_call_template=manual_call_template, manual=UtcpManual( From c23b594c8dd6fb86fe92596faf5322efa0c81df9 Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Thu, 4 Sep 2025 18:49:23 +0300 Subject: [PATCH 12/13] Fix mcp tests --- .../mcp/tests/test_mcp_http_transport.py | 18 ++++----- .../mcp/tests/test_mcp_transport.py | 40 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/plugins/communication_protocols/mcp/tests/test_mcp_http_transport.py b/plugins/communication_protocols/mcp/tests/test_mcp_http_transport.py index f8c626c..adaadb4 100644 --- a/plugins/communication_protocols/mcp/tests/test_mcp_http_transport.py +++ b/plugins/communication_protocols/mcp/tests/test_mcp_http_transport.py @@ -98,15 +98,15 @@ async def test_http_register_manual_discovers_tools( assert len(register_result.manual.tools) == 4 # Find the echo tool - echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None) + echo_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{HTTP_SERVER_NAME}.echo"), None) assert echo_tool is not None assert "echoes back its input" in echo_tool.description # Check for other tools tool_names = [tool.name for tool in register_result.manual.tools] - assert "greet" in tool_names - assert "list_items" in tool_names - assert "add_numbers" in tool_names + assert f"{HTTP_SERVER_NAME}.greet" in tool_names + assert f"{HTTP_SERVER_NAME}.list_items" in tool_names + assert f"{HTTP_SERVER_NAME}.add_numbers" in tool_names @pytest.mark.asyncio @@ -120,7 +120,7 @@ async def test_http_structured_output( await transport.register_manual(None, http_mcp_provider) # Call the echo tool and verify the result - result = await transport.call_tool(None, "echo", {"message": "http_test"}, http_mcp_provider) + result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "http_test"}, http_mcp_provider) assert result == {"reply": "you said: http_test"} @@ -135,7 +135,7 @@ async def test_http_unstructured_output( await transport.register_manual(None, http_mcp_provider) # Call the greet tool and verify the result - result = await transport.call_tool(None, "greet", {"name": "Alice"}, http_mcp_provider) + result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.greet", {"name": "Alice"}, http_mcp_provider) assert result == "Hello, Alice!" @@ -150,7 +150,7 @@ async def test_http_list_output( await transport.register_manual(None, http_mcp_provider) # Call the list_items tool and verify the result - result = await transport.call_tool(None, "list_items", {"count": 3}, http_mcp_provider) + result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.list_items", {"count": 3}, http_mcp_provider) assert isinstance(result, list) assert len(result) == 3 @@ -170,7 +170,7 @@ async def test_http_numeric_output( await transport.register_manual(None, http_mcp_provider) # Call the add_numbers tool and verify the result - result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, http_mcp_provider) + result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, http_mcp_provider) assert result == 12 @@ -191,5 +191,5 @@ async def test_http_deregister_manual( await transport.deregister_manual(None, http_mcp_provider) # Should still be able to call tools since we create fresh sessions - result = await transport.call_tool(None, "echo", {"message": "test"}, http_mcp_provider) + result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "test"}, http_mcp_provider) assert result == {"reply": "you said: test"} diff --git a/plugins/communication_protocols/mcp/tests/test_mcp_transport.py b/plugins/communication_protocols/mcp/tests/test_mcp_transport.py index 4af2691..cbd6073 100644 --- a/plugins/communication_protocols/mcp/tests/test_mcp_transport.py +++ b/plugins/communication_protocols/mcp/tests/test_mcp_transport.py @@ -55,15 +55,15 @@ async def test_register_manual_discovers_tools(transport: McpCommunicationProtoc assert len(register_result.manual.tools) == 4 # Find the echo tool - echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None) + echo_tool = next((tool for tool in register_result.manual.tools if tool.name ==f"{SERVER_NAME}.echo"), None) assert echo_tool is not None assert "echoes back its input" in echo_tool.description # Check for other tools tool_names = [tool.name for tool in register_result.manual.tools] - assert "greet" in tool_names - assert "list_items" in tool_names - assert "add_numbers" in tool_names + assert f"{SERVER_NAME}.greet" in tool_names + assert f"{SERVER_NAME}.list_items" in tool_names + assert f"{SERVER_NAME}.add_numbers" in tool_names @pytest.mark.asyncio @@ -71,7 +71,7 @@ async def test_call_tool_succeeds(transport: McpCommunicationProtocol, mcp_manua """Verify a successful tool call after registration.""" await transport.register_manual(None, mcp_manual) - result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual) assert result == {"reply": "you said: test"} @@ -79,7 +79,7 @@ async def test_call_tool_succeeds(transport: McpCommunicationProtocol, mcp_manua @pytest.mark.asyncio async def test_call_tool_works_without_register(transport: McpCommunicationProtocol, mcp_manual: McpCallTemplate): """Verify that calling a tool works without prior registration in session-per-operation mode.""" - result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual) assert result == {"reply": "you said: test"} @@ -88,7 +88,7 @@ async def test_structured_output_tool(transport: McpCommunicationProtocol, mcp_m """Test that tools with structured output (TypedDict) work correctly.""" await transport.register_manual(None, mcp_manual) - result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual) assert result == {"reply": "you said: test"} @@ -97,7 +97,7 @@ async def test_unstructured_string_output(transport: McpCommunicationProtocol, m """Test that tools returning plain strings work correctly.""" await transport.register_manual(None, mcp_manual) - result = await transport.call_tool(None, "greet", {"name": "Alice"}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.greet", {"name": "Alice"}, mcp_manual) assert result == "Hello, Alice!" @@ -106,7 +106,7 @@ async def test_list_output(transport: McpCommunicationProtocol, mcp_manual: McpC """Test that tools returning lists work correctly.""" await transport.register_manual(None, mcp_manual) - result = await transport.call_tool(None, "list_items", {"count": 3}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.list_items", {"count": 3}, mcp_manual) assert isinstance(result, list) assert len(result) == 3 @@ -118,7 +118,7 @@ async def test_numeric_output(transport: McpCommunicationProtocol, mcp_manual: M """Test that tools returning numeric values work correctly.""" await transport.register_manual(None, mcp_manual) - result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, mcp_manual) assert result == 12 @@ -132,7 +132,7 @@ async def test_deregister_manual(transport: McpCommunicationProtocol, mcp_manual await transport.deregister_manual(None, mcp_manual) - result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual) + result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual) assert result == {"reply": "you said: test"} @@ -145,7 +145,7 @@ async def test_register_resources_as_tools_disabled(transport: McpCommunicationP # Check that no resource tools are present tool_names = [tool.name for tool in register_result.manual.tools] - resource_tools = [name for name in tool_names if name.startswith("resource_")] + resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")] assert len(resource_tools) == 0 @@ -160,13 +160,13 @@ async def test_register_resources_as_tools_enabled(transport: McpCommunicationPr # Check that resource tools are present tool_names = [tool.name for tool in register_result.manual.tools] - resource_tools = [name for name in tool_names if name.startswith("resource_")] + resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")] assert len(resource_tools) == 2 - assert "resource_get_test_document" in resource_tools - assert "resource_get_config" in resource_tools + assert f"{SERVER_NAME}.resource_get_test_document" in resource_tools + assert f"{SERVER_NAME}.resource_get_config" in resource_tools # Check resource tool properties - test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == "resource_get_test_document"), None) + test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{SERVER_NAME}.resource_get_test_document"), None) assert test_doc_tool is not None assert "Read resource:" in test_doc_tool.description assert "file://test_document.txt" in test_doc_tool.description @@ -179,7 +179,7 @@ async def test_call_resource_tool(transport: McpCommunicationProtocol, mcp_manua await transport.register_manual(None, mcp_manual_with_resources) # Call the test document resource - result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources) + result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources) # Check that we get the resource content assert isinstance(result, dict) @@ -207,7 +207,7 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc await transport.register_manual(None, mcp_manual_with_resources) # Call the config.json resource - result = await transport.call_tool(None, "resource_get_config", {}, mcp_manual_with_resources) + result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_config", {}, mcp_manual_with_resources) # Check that we get the resource content assert isinstance(result, dict) @@ -232,14 +232,14 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc async def test_call_nonexistent_resource_tool(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate): """Verify that calling a non-existent resource tool raises an error.""" with pytest.raises(ValueError, match="Resource 'nonexistent' not found in any configured server"): - await transport.call_tool(None, "resource_nonexistent", {}, mcp_manual_with_resources) + await transport.call_tool(None, f"{SERVER_NAME}.resource_nonexistent", {}, mcp_manual_with_resources) @pytest.mark.asyncio async def test_resource_tool_without_registration(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate): """Verify that resource tools work even without prior registration.""" # Don't register the manual first - test direct call - result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources) + result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources) # Should still work and return content assert isinstance(result, dict) From 1320bf52048e0a321a682cc829e56504f1a9e38c Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Thu, 4 Sep 2025 18:50:50 +0300 Subject: [PATCH 13/13] Replace handlers with hasHandlers() --- core/src/utcp/__init__.py | 2 +- .../src/utcp_http/streamable_http_communication_protocol.py | 2 +- .../socket/src/utcp_socket/tcp_communication_protocol.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/utcp/__init__.py b/core/src/utcp/__init__.py index d6f3c35..cfe0dd4 100644 --- a/core/src/utcp/__init__.py +++ b/core/src/utcp/__init__.py @@ -3,7 +3,7 @@ logger = logging.getLogger("utcp") -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) diff --git a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py index df8e86c..2ba868e 100644 --- a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py +++ b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler) diff --git a/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py b/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py index 9db2c13..cab2665 100644 --- a/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py +++ b/plugins/communication_protocols/socket/src/utcp_socket/tcp_communication_protocol.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -if not logger.handlers: # Only add default handler if user didn't configure logging +if not logger.hasHandlers(): # Only add default handler if user didn't configure logging handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s")) logger.addHandler(handler)