strands-mcp-server 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of strands-mcp-server might be problematic. Click here for more details.

@@ -0,0 +1,525 @@
1
+ """MCP Client Tool for Strands Agents.
2
+
3
+ Test and interact with MCP servers from within a Strands Agent. This tool provides
4
+ a complete MCP client implementation that can connect to any MCP server (including
5
+ servers created with the mcp_server tool) and use their exposed tools.
6
+
7
+ Key Features:
8
+ - **Multiple Transports**: HTTP (streamable), stdio, SSE
9
+ - **Connection Management**: Connect, disconnect, list connections
10
+ - **Tool Discovery**: List available tools from connected servers
11
+ - **Tool Execution**: Call tools on remote servers
12
+ - **Session Persistence**: Maintain connections across multiple operations
13
+
14
+ Example:
15
+ ```python
16
+ from strands import Agent
17
+ from tools.mcp_client import mcp_client
18
+ from tools.mcp_server import mcp_server
19
+
20
+ agent = Agent(tools=[mcp_client, mcp_server])
21
+
22
+ # Start a server
23
+ agent("start mcp server on port 8000")
24
+
25
+ # Connect to it
26
+ agent("connect to mcp server at http://localhost:8000/mcp as test-server")
27
+
28
+ # List its tools
29
+ agent("list tools from test-server")
30
+
31
+ # Call a tool
32
+ agent("call calculator tool on test-server with expression: 2 + 2")
33
+ ```
34
+
35
+
36
+ References:
37
+ - MCP Specification: https://spec.modelcontextprotocol.io/
38
+ - MCP Client SDK: python-sdk/src/mcp/client/
39
+ """
40
+
41
+ import logging
42
+ from typing import Any, Optional
43
+
44
+ from strands import tool
45
+
46
+ # Global state for managing MCP connections
47
+ _client_connections: dict[str, Any] = {}
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ @tool
53
+ def mcp_client(
54
+ action: str,
55
+ connection_id: Optional[str] = None,
56
+ transport: Optional[str] = None,
57
+ server_url: Optional[str] = None,
58
+ command: Optional[str] = None,
59
+ args: Optional[list[str]] = None,
60
+ tool_name: Optional[str] = None,
61
+ tool_args: Optional[dict[str, Any]] = None,
62
+ ) -> dict[str, Any]:
63
+ """Test and interact with MCP servers.
64
+
65
+ This tool provides a complete MCP client implementation for testing and using
66
+ MCP servers from within a Strands Agent.
67
+
68
+ Args:
69
+ action: Action to perform - "connect", "disconnect", "list_tools", "call_tool", "list_connections"
70
+ connection_id: Unique identifier for this connection
71
+ transport: Transport type - "http", "stdio", or "sse"
72
+ server_url: URL for HTTP/SSE transport (e.g., "http://localhost:8000/mcp")
73
+ command: Command for stdio transport (e.g., "python")
74
+ args: Arguments for stdio command (e.g., ["mcp_server_stdio.py"])
75
+ tool_name: Name of tool to call (for call_tool action)
76
+ tool_args: Arguments to pass to tool (for call_tool action)
77
+
78
+ Returns:
79
+ Result dictionary with status and content
80
+
81
+ Examples:
82
+ # Connect to HTTP server
83
+ mcp_client(
84
+ action="connect",
85
+ connection_id="my-server",
86
+ transport="http",
87
+ server_url="http://localhost:8000/mcp"
88
+ )
89
+
90
+ # Connect to stdio server
91
+ mcp_client(
92
+ action="connect",
93
+ connection_id="stdio-server",
94
+ transport="stdio",
95
+ command="python",
96
+ args=["mcp_server_stdio.py"]
97
+ )
98
+
99
+ # List tools from connection
100
+ mcp_client(action="list_tools", connection_id="my-server")
101
+
102
+ # Call a tool
103
+ mcp_client(
104
+ action="call_tool",
105
+ connection_id="my-server",
106
+ tool_name="calculator",
107
+ tool_args={"expression": "2 + 2"}
108
+ )
109
+
110
+ # List all connections
111
+ mcp_client(action="list_connections")
112
+
113
+ # Disconnect
114
+ mcp_client(action="disconnect", connection_id="my-server")
115
+
116
+ Notes:
117
+ - stdio transport: Server must be launchable as subprocess
118
+ - HTTP transport: Server must be already running
119
+ - Connections are maintained in global state for reuse
120
+ """
121
+ if action == "connect":
122
+ return _connect(connection_id, transport, server_url, command, args)
123
+ elif action == "disconnect":
124
+ return _disconnect(connection_id)
125
+ elif action == "list_tools":
126
+ return _list_tools(connection_id)
127
+ elif action == "call_tool":
128
+ return _call_tool(connection_id, tool_name, tool_args)
129
+ elif action == "list_connections":
130
+ return _list_connections()
131
+ else:
132
+ return {
133
+ "status": "error",
134
+ "content": [
135
+ {
136
+ "text": f"❌ Unknown action: {action}\n\n"
137
+ "Available actions: connect, disconnect, list_tools, call_tool, list_connections"
138
+ }
139
+ ],
140
+ }
141
+
142
+
143
+ def _connect(
144
+ connection_id: Optional[str],
145
+ transport: Optional[str],
146
+ server_url: Optional[str],
147
+ command: Optional[str],
148
+ args: Optional[list[str]],
149
+ ) -> dict[str, Any]:
150
+ """Connect to an MCP server.
151
+
152
+ This function establishes a connection to an MCP server using the specified
153
+ transport type. The connection is stored in global state for reuse.
154
+
155
+ Follows patterns from:
156
+ - Strands MCPClient: sdk-python/src/strands/tools/mcp/mcp_client.py
157
+ - MCP client transports: python-sdk/src/mcp/client/
158
+ """
159
+ try:
160
+ if not connection_id:
161
+ return {
162
+ "status": "error",
163
+ "content": [{"text": "❌ connection_id is required"}],
164
+ }
165
+
166
+ if not transport:
167
+ return {
168
+ "status": "error",
169
+ "content": [{"text": "❌ transport is required (http, stdio, or sse)"}],
170
+ }
171
+
172
+ if connection_id in _client_connections:
173
+ return {
174
+ "status": "error",
175
+ "content": [{"text": f"❌ Connection '{connection_id}' already exists"}],
176
+ }
177
+
178
+ # Import MCP client components
179
+ from mcp.client.session import ClientSession
180
+ from mcp.client.stdio import StdioServerParameters, stdio_client
181
+ from mcp.client.streamable_http import streamablehttp_client
182
+
183
+ if transport == "http":
184
+ if not server_url:
185
+ return {
186
+ "status": "error",
187
+ "content": [{"text": "❌ server_url is required for HTTP transport"}],
188
+ }
189
+
190
+ logger.debug(f"Connecting to HTTP server: {server_url}")
191
+
192
+ # Create transport callable following Strands MCPClient pattern
193
+ def transport_callable():
194
+ return streamablehttp_client(server_url)
195
+
196
+ connection_info = {
197
+ "transport": "http",
198
+ "server_url": server_url,
199
+ "transport_callable": transport_callable,
200
+ "session": None, # Will be initialized on first use
201
+ }
202
+
203
+ elif transport == "stdio":
204
+ if not command:
205
+ return {
206
+ "status": "error",
207
+ "content": [{"text": "❌ command is required for stdio transport"}],
208
+ }
209
+
210
+ logger.debug(f"Connecting to stdio server: {command} {args or []}")
211
+
212
+ # Create stdio server parameters
213
+ server_params = StdioServerParameters(command=command, args=args or [], env=None)
214
+
215
+ # Create transport callable
216
+ def transport_callable():
217
+ return stdio_client(server_params)
218
+
219
+ connection_info = {
220
+ "transport": "stdio",
221
+ "command": command,
222
+ "args": args,
223
+ "transport_callable": transport_callable,
224
+ "session": None,
225
+ }
226
+
227
+ elif transport == "sse":
228
+ if not server_url:
229
+ return {
230
+ "status": "error",
231
+ "content": [{"text": "❌ server_url is required for SSE transport"}],
232
+ }
233
+
234
+ logger.debug(f"Connecting to SSE server: {server_url}")
235
+
236
+ from mcp.client.sse import sse_client
237
+
238
+ def transport_callable():
239
+ return sse_client(server_url)
240
+
241
+ connection_info = {
242
+ "transport": "sse",
243
+ "server_url": server_url,
244
+ "transport_callable": transport_callable,
245
+ "session": None,
246
+ }
247
+
248
+ else:
249
+ return {
250
+ "status": "error",
251
+ "content": [{"text": f"❌ Unknown transport: {transport}\n\nSupported: http, stdio, sse"}],
252
+ }
253
+
254
+ # Test the connection by listing tools
255
+ logger.debug(f"Testing connection by listing tools...")
256
+ tools = []
257
+ try:
258
+ # Create a temporary session to test and get tool count
259
+ import asyncio
260
+
261
+ async def test_connection():
262
+ async with connection_info["transport_callable"]() as (
263
+ read_stream,
264
+ write_stream,
265
+ get_session_id, # StreamableHTTP returns 3 values
266
+ ):
267
+ async with ClientSession(read_stream, write_stream) as session:
268
+ await session.initialize()
269
+ result = await session.list_tools()
270
+ return result.tools
271
+
272
+ tools = asyncio.run(test_connection())
273
+ logger.debug(f"Successfully connected, found {len(tools)} tools")
274
+
275
+ except Exception as e:
276
+ logger.exception("Failed to connect to MCP server")
277
+ return {
278
+ "status": "error",
279
+ "content": [{"text": f"❌ Failed to connect: {str(e)}"}],
280
+ }
281
+
282
+ # Store connection
283
+ _client_connections[connection_id] = connection_info
284
+
285
+ # Build response
286
+ tool_list = "\n".join(f" • {tool.name}" for tool in tools[:10])
287
+ if len(tools) > 10:
288
+ tool_list += f"\n ... and {len(tools) - 10} more"
289
+
290
+ transport_info = (
291
+ f"URL: {server_url}" if transport in ["http", "sse"] else f"Command: {command} {' '.join(args or [])}"
292
+ )
293
+
294
+ message = (
295
+ f"✅ Connected to MCP server '{connection_id}'\n\n"
296
+ f"📊 Transport: {transport}\n"
297
+ f"🔗 {transport_info}\n"
298
+ f"🔧 Available tools ({len(tools)}):\n"
299
+ f"{tool_list}"
300
+ )
301
+
302
+ return {"status": "success", "content": [{"text": message}]}
303
+
304
+ except Exception as e:
305
+ logger.exception("Error in connect action")
306
+ return {
307
+ "status": "error",
308
+ "content": [{"text": f"❌ Error: {str(e)}"}],
309
+ }
310
+
311
+
312
+ def _disconnect(connection_id: Optional[str]) -> dict[str, Any]:
313
+ """Disconnect from an MCP server.
314
+
315
+ Removes the connection from global state. The actual transport cleanup
316
+ happens automatically via context managers.
317
+ """
318
+ try:
319
+ if not connection_id:
320
+ return {
321
+ "status": "error",
322
+ "content": [{"text": "❌ connection_id is required"}],
323
+ }
324
+
325
+ if connection_id not in _client_connections:
326
+ return {
327
+ "status": "error",
328
+ "content": [{"text": f"❌ Connection '{connection_id}' not found"}],
329
+ }
330
+
331
+ # Remove connection
332
+ del _client_connections[connection_id]
333
+ logger.debug(f"Disconnected from '{connection_id}'")
334
+
335
+ return {
336
+ "status": "success",
337
+ "content": [{"text": f"✅ Disconnected from '{connection_id}'"}],
338
+ }
339
+
340
+ except Exception as e:
341
+ logger.exception("Error in disconnect action")
342
+ return {
343
+ "status": "error",
344
+ "content": [{"text": f"❌ Error: {str(e)}"}],
345
+ }
346
+
347
+
348
+ def _list_tools(connection_id: Optional[str]) -> dict[str, Any]:
349
+ """List tools from a connected MCP server.
350
+
351
+ This creates a temporary session to query the server for its available tools.
352
+ """
353
+ try:
354
+ if not connection_id:
355
+ return {
356
+ "status": "error",
357
+ "content": [{"text": "❌ connection_id is required"}],
358
+ }
359
+
360
+ if connection_id not in _client_connections:
361
+ return {
362
+ "status": "error",
363
+ "content": [{"text": f"❌ Connection '{connection_id}' not found"}],
364
+ }
365
+
366
+ connection_info = _client_connections[connection_id]
367
+ logger.debug(f"Listing tools from '{connection_id}'")
368
+
369
+ # Create session and list tools
370
+ import asyncio
371
+
372
+ from mcp.client.session import ClientSession
373
+
374
+ async def list_tools_async():
375
+ async with connection_info["transport_callable"]() as (
376
+ read_stream,
377
+ write_stream,
378
+ get_session_id, # StreamableHTTP returns 3 values
379
+ ):
380
+ async with ClientSession(read_stream, write_stream) as session:
381
+ await session.initialize()
382
+ result = await session.list_tools()
383
+ return result.tools
384
+
385
+ tools = asyncio.run(list_tools_async())
386
+ logger.debug(f"Found {len(tools)} tools")
387
+
388
+ # Build detailed tool list
389
+ tool_details = []
390
+ for tool in tools:
391
+ details = f"**{tool.name}**"
392
+ if tool.description:
393
+ details += f"\n Description: {tool.description}"
394
+ if tool.inputSchema:
395
+ # Show required parameters
396
+ schema = tool.inputSchema
397
+ if "required" in schema:
398
+ details += f"\n Required: {', '.join(schema['required'])}"
399
+ if "properties" in schema:
400
+ details += f"\n Parameters: {', '.join(schema['properties'].keys())}"
401
+ tool_details.append(details)
402
+
403
+ tools_text = "\n\n".join(tool_details)
404
+
405
+ message = f"📋 **Tools from '{connection_id}'**\n\n" f"Found {len(tools)} tools:\n\n" f"{tools_text}"
406
+
407
+ return {"status": "success", "content": [{"text": message}]}
408
+
409
+ except Exception as e:
410
+ logger.exception("Error listing tools")
411
+ return {
412
+ "status": "error",
413
+ "content": [{"text": f"❌ Error: {str(e)}"}],
414
+ }
415
+
416
+
417
+ def _call_tool(
418
+ connection_id: Optional[str],
419
+ tool_name: Optional[str],
420
+ tool_args: Optional[dict[str, Any]],
421
+ ) -> dict[str, Any]:
422
+ """Call a tool on a connected MCP server.
423
+
424
+ This establishes a session, calls the specified tool with provided arguments,
425
+ and returns the result.
426
+ """
427
+ try:
428
+ if not connection_id:
429
+ return {
430
+ "status": "error",
431
+ "content": [{"text": "❌ connection_id is required"}],
432
+ }
433
+
434
+ if not tool_name:
435
+ return {
436
+ "status": "error",
437
+ "content": [{"text": "❌ tool_name is required"}],
438
+ }
439
+
440
+ if connection_id not in _client_connections:
441
+ return {
442
+ "status": "error",
443
+ "content": [{"text": f"❌ Connection '{connection_id}' not found"}],
444
+ }
445
+
446
+ connection_info = _client_connections[connection_id]
447
+ logger.debug(f"Calling tool '{tool_name}' on '{connection_id}' with args: {tool_args}")
448
+
449
+ # Call the tool
450
+ import asyncio
451
+
452
+ from mcp.client.session import ClientSession
453
+
454
+ async def call_tool_async():
455
+ async with connection_info["transport_callable"]() as (
456
+ read_stream,
457
+ write_stream,
458
+ get_session_id,
459
+ ):
460
+ async with ClientSession(read_stream, write_stream) as session:
461
+ await session.initialize()
462
+ result = await session.call_tool(tool_name, tool_args or {})
463
+ return result
464
+
465
+ result = asyncio.run(call_tool_async())
466
+ logger.debug(f"Tool call complete, got {len(result.content)} content items")
467
+
468
+ # Extract result content
469
+ result_text = []
470
+ for content in result.content:
471
+ if hasattr(content, "text"):
472
+ result_text.append(content.text)
473
+ else:
474
+ result_text.append(str(content))
475
+
476
+ combined_result = "\n".join(result_text)
477
+
478
+ message = f"✅ **Tool '{tool_name}' executed on '{connection_id}'**\n\n" f"Result:\n{combined_result}"
479
+
480
+ return {"status": "success", "content": [{"text": message}]}
481
+
482
+ except Exception as e:
483
+ logger.exception(f"Error calling tool '{tool_name}'")
484
+ return {
485
+ "status": "error",
486
+ "content": [{"text": f"❌ Error: {str(e)}"}],
487
+ }
488
+
489
+
490
+ def _list_connections() -> dict[str, Any]:
491
+ """List all active MCP connections.
492
+
493
+ Shows connection details including transport type and available tools.
494
+ """
495
+ try:
496
+ if not _client_connections:
497
+ return {
498
+ "status": "success",
499
+ "content": [{"text": "📭 No active MCP connections"}],
500
+ }
501
+
502
+ lines = [f"📡 **Active MCP Connections** ({len(_client_connections)})\n"]
503
+
504
+ for conn_id, conn_info in _client_connections.items():
505
+ lines.append(f"\n**{conn_id}**")
506
+ lines.append(f" • Transport: {conn_info['transport']}")
507
+
508
+ if conn_info["transport"] == "http":
509
+ lines.append(f" • URL: {conn_info['server_url']}")
510
+ elif conn_info["transport"] == "sse":
511
+ lines.append(f" • URL: {conn_info['server_url']}")
512
+ elif conn_info["transport"] == "stdio":
513
+ cmd = f"{conn_info['command']} {' '.join(conn_info.get('args', []))}"
514
+ lines.append(f" • Command: {cmd}")
515
+
516
+ message = "\n".join(lines)
517
+
518
+ return {"status": "success", "content": [{"text": message}]}
519
+
520
+ except Exception as e:
521
+ logger.exception("Error listing connections")
522
+ return {
523
+ "status": "error",
524
+ "content": [{"text": f"❌ Error: {str(e)}"}],
525
+ }