Skip to content

Commit fe3aad0

Browse files
authored
Merge pull request #57 from universal-tool-calling-protocol/hotfix/mcp-register-tools
MCP register tools
2 parents e4627ab + 1320bf5 commit fe3aad0

File tree

12 files changed

+250
-113
lines changed

12 files changed

+250
-113
lines changed

core/src/utcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
logger = logging.getLogger("utcp")
55

6-
if not logger.handlers: # Only add default handler if user didn't configure logging
6+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
77
handler = logging.StreamHandler(sys.stderr)
88
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
99
logger.addHandler(handler)

core/src/utcp/implementations/utcp_client_implementation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
110110
raise ValueError(f"No registered communication protocol of type {manual_call_template.call_template_type} found, available types: {CommunicationProtocol.communication_protocols.keys()}")
111111

112112
result = await CommunicationProtocol.communication_protocols[manual_call_template.call_template_type].register_manual(self, manual_call_template)
113-
113+
114114
if result.success:
115115
for tool in result.manual.tools:
116116
if not tool.name.startswith(manual_call_template.name + "."):

plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import json
2424
import os
2525
import shlex
26+
import sys
2627
from typing import Dict, Any, List, Optional, Callable, AsyncGenerator
2728

2829
from utcp.interfaces.communication_protocol import CommunicationProtocol
@@ -35,6 +36,12 @@
3536

3637
logger = logging.getLogger(__name__)
3738

39+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
40+
handler = logging.StreamHandler(sys.stderr)
41+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
42+
logger.addHandler(handler)
43+
logger.setLevel(logging.INFO)
44+
3845

3946
class CliCommunicationProtocol(CommunicationProtocol):
4047
"""REQUIRED

plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Dict, Any, List, Optional, Callable
23
import aiohttp
34
import asyncio
@@ -12,6 +13,13 @@
1213

1314
logger = logging.getLogger(__name__)
1415

16+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
17+
handler = logging.StreamHandler(sys.stderr)
18+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
19+
logger.addHandler(handler)
20+
logger.setLevel(logging.INFO)
21+
22+
1523
class GraphQLClientTransport(ClientTransportInterface):
1624
"""
1725
Simple, robust, production-ready GraphQL transport using gql.

plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Request/response handling with proper error management
1313
"""
1414

15+
import sys
1516
from typing import Dict, Any, List, Optional, Callable, AsyncGenerator
1617
import aiohttp
1718
import json
@@ -36,6 +37,12 @@
3637

3738
logger = logging.getLogger(__name__)
3839

40+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
41+
handler = logging.StreamHandler(sys.stderr)
42+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
43+
logger.addHandler(handler)
44+
logger.setLevel(logging.INFO)
45+
3946
class HttpCommunicationProtocol(CommunicationProtocol):
4047
"""REQUIRED
4148
HTTP communication protocol implementation for UTCP client.

plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Dict, Any, List, Optional, Callable, AsyncIterator, AsyncGenerator
23
import aiohttp
34
import json
@@ -21,6 +22,12 @@
2122

2223
logger = logging.getLogger(__name__)
2324

25+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
26+
handler = logging.StreamHandler(sys.stderr)
27+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
28+
logger.addHandler(handler)
29+
logger.setLevel(logging.INFO)
30+
2431
class SseCommunicationProtocol(CommunicationProtocol):
2532
"""REQUIRED
2633
SSE communication protocol implementation for UTCP client.

plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Dict, Any, List, Optional, Callable, AsyncIterator, Tuple, AsyncGenerator
23
import aiohttp
34
import json
@@ -18,6 +19,12 @@
1819

1920
logger = logging.getLogger(__name__)
2021

22+
if not logger.hasHandlers(): # Only add default handler if user didn't configure logging
23+
handler = logging.StreamHandler(sys.stderr)
24+
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s"))
25+
logger.addHandler(handler)
26+
logger.setLevel(logging.INFO)
27+
2128
class StreamableHttpCommunicationProtocol(CommunicationProtocol):
2229
"""REQUIRED
2330
Streamable HTTP communication protocol implementation for UTCP client.

plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py

Lines changed: 168 additions & 82 deletions
Large diffs are not rendered by default.

plugins/communication_protocols/mcp/tests/test_mcp_http_transport.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ async def test_http_register_manual_discovers_tools(
9898
assert len(register_result.manual.tools) == 4
9999

100100
# Find the echo tool
101-
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None)
101+
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{HTTP_SERVER_NAME}.echo"), None)
102102
assert echo_tool is not None
103103
assert "echoes back its input" in echo_tool.description
104104

105105
# Check for other tools
106106
tool_names = [tool.name for tool in register_result.manual.tools]
107-
assert "greet" in tool_names
108-
assert "list_items" in tool_names
109-
assert "add_numbers" in tool_names
107+
assert f"{HTTP_SERVER_NAME}.greet" in tool_names
108+
assert f"{HTTP_SERVER_NAME}.list_items" in tool_names
109+
assert f"{HTTP_SERVER_NAME}.add_numbers" in tool_names
110110

111111

112112
@pytest.mark.asyncio
@@ -120,7 +120,7 @@ async def test_http_structured_output(
120120
await transport.register_manual(None, http_mcp_provider)
121121

122122
# Call the echo tool and verify the result
123-
result = await transport.call_tool(None, "echo", {"message": "http_test"}, http_mcp_provider)
123+
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "http_test"}, http_mcp_provider)
124124
assert result == {"reply": "you said: http_test"}
125125

126126

@@ -135,7 +135,7 @@ async def test_http_unstructured_output(
135135
await transport.register_manual(None, http_mcp_provider)
136136

137137
# Call the greet tool and verify the result
138-
result = await transport.call_tool(None, "greet", {"name": "Alice"}, http_mcp_provider)
138+
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.greet", {"name": "Alice"}, http_mcp_provider)
139139
assert result == "Hello, Alice!"
140140

141141

@@ -150,7 +150,7 @@ async def test_http_list_output(
150150
await transport.register_manual(None, http_mcp_provider)
151151

152152
# Call the list_items tool and verify the result
153-
result = await transport.call_tool(None, "list_items", {"count": 3}, http_mcp_provider)
153+
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.list_items", {"count": 3}, http_mcp_provider)
154154

155155
assert isinstance(result, list)
156156
assert len(result) == 3
@@ -170,7 +170,7 @@ async def test_http_numeric_output(
170170
await transport.register_manual(None, http_mcp_provider)
171171

172172
# Call the add_numbers tool and verify the result
173-
result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, http_mcp_provider)
173+
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, http_mcp_provider)
174174

175175
assert result == 12
176176

@@ -191,5 +191,5 @@ async def test_http_deregister_manual(
191191
await transport.deregister_manual(None, http_mcp_provider)
192192

193193
# Should still be able to call tools since we create fresh sessions
194-
result = await transport.call_tool(None, "echo", {"message": "test"}, http_mcp_provider)
194+
result = await transport.call_tool(None, f"{HTTP_SERVER_NAME}.echo", {"message": "test"}, http_mcp_provider)
195195
assert result == {"reply": "you said: test"}

plugins/communication_protocols/mcp/tests/test_mcp_transport.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,31 +55,31 @@ async def test_register_manual_discovers_tools(transport: McpCommunicationProtoc
5555
assert len(register_result.manual.tools) == 4
5656

5757
# Find the echo tool
58-
echo_tool = next((tool for tool in register_result.manual.tools if tool.name == "echo"), None)
58+
echo_tool = next((tool for tool in register_result.manual.tools if tool.name ==f"{SERVER_NAME}.echo"), None)
5959
assert echo_tool is not None
6060
assert "echoes back its input" in echo_tool.description
6161

6262
# Check for other tools
6363
tool_names = [tool.name for tool in register_result.manual.tools]
64-
assert "greet" in tool_names
65-
assert "list_items" in tool_names
66-
assert "add_numbers" in tool_names
64+
assert f"{SERVER_NAME}.greet" in tool_names
65+
assert f"{SERVER_NAME}.list_items" in tool_names
66+
assert f"{SERVER_NAME}.add_numbers" in tool_names
6767

6868

6969
@pytest.mark.asyncio
7070
async def test_call_tool_succeeds(transport: McpCommunicationProtocol, mcp_manual: McpCallTemplate):
7171
"""Verify a successful tool call after registration."""
7272
await transport.register_manual(None, mcp_manual)
7373

74-
result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
74+
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
7575

7676
assert result == {"reply": "you said: test"}
7777

7878

7979
@pytest.mark.asyncio
8080
async def test_call_tool_works_without_register(transport: McpCommunicationProtocol, mcp_manual: McpCallTemplate):
8181
"""Verify that calling a tool works without prior registration in session-per-operation mode."""
82-
result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
82+
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
8383
assert result == {"reply": "you said: test"}
8484

8585

@@ -88,7 +88,7 @@ async def test_structured_output_tool(transport: McpCommunicationProtocol, mcp_m
8888
"""Test that tools with structured output (TypedDict) work correctly."""
8989
await transport.register_manual(None, mcp_manual)
9090

91-
result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
91+
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
9292
assert result == {"reply": "you said: test"}
9393

9494

@@ -97,7 +97,7 @@ async def test_unstructured_string_output(transport: McpCommunicationProtocol, m
9797
"""Test that tools returning plain strings work correctly."""
9898
await transport.register_manual(None, mcp_manual)
9999

100-
result = await transport.call_tool(None, "greet", {"name": "Alice"}, mcp_manual)
100+
result = await transport.call_tool(None, f"{SERVER_NAME}.greet", {"name": "Alice"}, mcp_manual)
101101
assert result == "Hello, Alice!"
102102

103103

@@ -106,7 +106,7 @@ async def test_list_output(transport: McpCommunicationProtocol, mcp_manual: McpC
106106
"""Test that tools returning lists work correctly."""
107107
await transport.register_manual(None, mcp_manual)
108108

109-
result = await transport.call_tool(None, "list_items", {"count": 3}, mcp_manual)
109+
result = await transport.call_tool(None, f"{SERVER_NAME}.list_items", {"count": 3}, mcp_manual)
110110

111111
assert isinstance(result, list)
112112
assert len(result) == 3
@@ -118,7 +118,7 @@ async def test_numeric_output(transport: McpCommunicationProtocol, mcp_manual: M
118118
"""Test that tools returning numeric values work correctly."""
119119
await transport.register_manual(None, mcp_manual)
120120

121-
result = await transport.call_tool(None, "add_numbers", {"a": 5, "b": 7}, mcp_manual)
121+
result = await transport.call_tool(None, f"{SERVER_NAME}.add_numbers", {"a": 5, "b": 7}, mcp_manual)
122122

123123
assert result == 12
124124

@@ -132,7 +132,7 @@ async def test_deregister_manual(transport: McpCommunicationProtocol, mcp_manual
132132

133133
await transport.deregister_manual(None, mcp_manual)
134134

135-
result = await transport.call_tool(None, "echo", {"message": "test"}, mcp_manual)
135+
result = await transport.call_tool(None, f"{SERVER_NAME}.echo", {"message": "test"}, mcp_manual)
136136
assert result == {"reply": "you said: test"}
137137

138138

@@ -145,7 +145,7 @@ async def test_register_resources_as_tools_disabled(transport: McpCommunicationP
145145

146146
# Check that no resource tools are present
147147
tool_names = [tool.name for tool in register_result.manual.tools]
148-
resource_tools = [name for name in tool_names if name.startswith("resource_")]
148+
resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")]
149149
assert len(resource_tools) == 0
150150

151151

@@ -160,13 +160,13 @@ async def test_register_resources_as_tools_enabled(transport: McpCommunicationPr
160160

161161
# Check that resource tools are present
162162
tool_names = [tool.name for tool in register_result.manual.tools]
163-
resource_tools = [name for name in tool_names if name.startswith("resource_")]
163+
resource_tools = [name for name in tool_names if name.startswith(f"{SERVER_NAME}.resource_")]
164164
assert len(resource_tools) == 2
165-
assert "resource_get_test_document" in resource_tools
166-
assert "resource_get_config" in resource_tools
165+
assert f"{SERVER_NAME}.resource_get_test_document" in resource_tools
166+
assert f"{SERVER_NAME}.resource_get_config" in resource_tools
167167

168168
# Check resource tool properties
169-
test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == "resource_get_test_document"), None)
169+
test_doc_tool = next((tool for tool in register_result.manual.tools if tool.name == f"{SERVER_NAME}.resource_get_test_document"), None)
170170
assert test_doc_tool is not None
171171
assert "Read resource:" in test_doc_tool.description
172172
assert "file://test_document.txt" in test_doc_tool.description
@@ -179,7 +179,7 @@ async def test_call_resource_tool(transport: McpCommunicationProtocol, mcp_manua
179179
await transport.register_manual(None, mcp_manual_with_resources)
180180

181181
# Call the test document resource
182-
result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources)
182+
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources)
183183

184184
# Check that we get the resource content
185185
assert isinstance(result, dict)
@@ -207,7 +207,7 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc
207207
await transport.register_manual(None, mcp_manual_with_resources)
208208

209209
# Call the config.json resource
210-
result = await transport.call_tool(None, "resource_get_config", {}, mcp_manual_with_resources)
210+
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_config", {}, mcp_manual_with_resources)
211211

212212
# Check that we get the resource content
213213
assert isinstance(result, dict)
@@ -232,14 +232,14 @@ async def test_call_resource_tool_json_content(transport: McpCommunicationProtoc
232232
async def test_call_nonexistent_resource_tool(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate):
233233
"""Verify that calling a non-existent resource tool raises an error."""
234234
with pytest.raises(ValueError, match="Resource 'nonexistent' not found in any configured server"):
235-
await transport.call_tool(None, "resource_nonexistent", {}, mcp_manual_with_resources)
235+
await transport.call_tool(None, f"{SERVER_NAME}.resource_nonexistent", {}, mcp_manual_with_resources)
236236

237237

238238
@pytest.mark.asyncio
239239
async def test_resource_tool_without_registration(transport: McpCommunicationProtocol, mcp_manual_with_resources: McpCallTemplate):
240240
"""Verify that resource tools work even without prior registration."""
241241
# Don't register the manual first - test direct call
242-
result = await transport.call_tool(None, "resource_get_test_document", {}, mcp_manual_with_resources)
242+
result = await transport.call_tool(None, f"{SERVER_NAME}.resource_get_test_document", {}, mcp_manual_with_resources)
243243

244244
# Should still work and return content
245245
assert isinstance(result, dict)

0 commit comments

Comments
 (0)