gnosys-strata 1.1.4__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.
strata/tools.py ADDED
@@ -0,0 +1,714 @@
1
+ """Shared tool implementations for Strata MCP Router."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import traceback
7
+ from typing import List, Dict, Any
8
+
9
+ import mcp.types as types
10
+
11
+ from .mcp_client_manager import MCPClientManager
12
+ from .config import mcp_server_list
13
+ from .utils.shared_search import UniversalToolSearcher
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _build_error_response(error_msg: str, traceback_info: str = None, **extra) -> Dict[str, Any]:
19
+ """Build standardized error response with optional traceback.
20
+
21
+ Args:
22
+ error_msg: Human-readable error message
23
+ traceback_info: Optional full traceback string
24
+ **extra: Additional context fields
25
+
26
+ Returns:
27
+ Structured error dict with status, error, and optional traceback
28
+ """
29
+ response = {
30
+ "status": "error",
31
+ "error": error_msg,
32
+ **extra
33
+ }
34
+ if traceback_info:
35
+ response["traceback"] = traceback_info
36
+ return response
37
+
38
+ # Tool Names
39
+ TOOL_DISCOVER_SERVER_ACTIONS = "discover_server_actions"
40
+ TOOL_GET_ACTION_DETAILS = "get_action_details"
41
+ TOOL_EXECUTE_ACTION = "execute_action"
42
+ TOOL_SEARCH_DOCUMENTATION = "search_documentation"
43
+ TOOL_HANDLE_AUTH_FAILURE = "handle_auth_failure"
44
+ # Tool Names for Management
45
+ TOOL_MANAGE_SERVERS = "manage_servers"
46
+ TOOL_SEARCH_MCP_CATALOG = "search_mcp_catalog"
47
+
48
+
49
+ def get_tool_definitions(user_available_servers: List[str]) -> List[types.Tool]:
50
+ """Get tool definitions for the available servers."""
51
+ return [
52
+ types.Tool(
53
+ name=TOOL_DISCOVER_SERVER_ACTIONS,
54
+ description="**PREFERRED STARTING POINT**: Discover available actions from servers based on user query.",
55
+ inputSchema={
56
+ "type": "object",
57
+ "required": ["user_query", "server_names"],
58
+ "properties": {
59
+ "user_query": {
60
+ "type": "string",
61
+ "description": "Natural language user query to filter results.",
62
+ },
63
+ "server_names": {
64
+ "type": "array",
65
+ "items": {"type": "string", "enum": user_available_servers},
66
+ "description": "List of server names to discover actions from.",
67
+ },
68
+ },
69
+ },
70
+ ),
71
+ types.Tool(
72
+ name=TOOL_GET_ACTION_DETAILS,
73
+ description="Get detailed information about a specific action.",
74
+ inputSchema={
75
+ "type": "object",
76
+ "required": ["server_name", "action_name"],
77
+ "properties": {
78
+ "server_name": {
79
+ "type": "string",
80
+ "enum": user_available_servers,
81
+ "description": "The name of the server",
82
+ },
83
+ "action_name": {
84
+ "type": "string",
85
+ "description": "The name of the action/operation",
86
+ },
87
+ },
88
+ },
89
+ ),
90
+ types.Tool(
91
+ name=TOOL_MANAGE_SERVERS,
92
+ description="Manage MCP server connections and Sets.",
93
+ inputSchema={
94
+ "type": "object",
95
+ "properties": {
96
+ "list_configured_mcps": {
97
+ "type": "boolean",
98
+ "description": "If true, lists all configured servers with their status.",
99
+ },
100
+ "list_sets": {
101
+ "type": "boolean",
102
+ "description": "If true, lists all configured Sets and their servers.",
103
+ },
104
+ "connect": {
105
+ "type": "string",
106
+ "description": "Name of the server to connect (turn on).",
107
+ },
108
+ "connect_set": {
109
+ "type": "string",
110
+ "description": "Name of the Set to connect (turn on all servers in set).",
111
+ },
112
+ "connect_set_exclusive": {
113
+ "type": "boolean",
114
+ "description": "If true with connect_set, disconnects all other servers first.",
115
+ },
116
+ "search_sets": {
117
+ "type": "string",
118
+ "description": "Search set descriptions for matching sets.",
119
+ },
120
+ "upsert_set": {
121
+ "type": "object",
122
+ "description": "Create or update a Set.",
123
+ "properties": {
124
+ "name": {"type": "string"},
125
+ "servers": {
126
+ "type": "array",
127
+ "items": {"type": "string"}
128
+ },
129
+ "description": {"type": "string"},
130
+ "include_sets": {
131
+ "type": "array",
132
+ "items": {"type": "string"},
133
+ "description": "Other sets to include (composability)"
134
+ }
135
+ },
136
+ "required": ["name"]
137
+ },
138
+ "delete_set": {
139
+ "type": "string",
140
+ "description": "Name of the Set to delete.",
141
+ },
142
+ "disconnect": {
143
+ "type": "string",
144
+ "description": "Name of the server to disconnect (turn off).",
145
+ },
146
+ "disconnect_set": {
147
+ "type": "string",
148
+ "description": "Name of the Set to disconnect (turn off all servers in set).",
149
+ },
150
+ "disconnect_all": {
151
+ "type": "boolean",
152
+ "description": "If true, disconnects all servers.",
153
+ },
154
+ "populate_catalog": {
155
+ "type": "boolean",
156
+ "description": "If true, connects to all enabled servers, refreshes catalog cache, then disconnects. Use when adding new MCPs.",
157
+ },
158
+ },
159
+ },
160
+ ),
161
+ types.Tool(
162
+ name=TOOL_SEARCH_MCP_CATALOG,
163
+ description="Search for tools in the offline catalog and discover Sets/Collections.",
164
+ inputSchema={
165
+ "type": "object",
166
+ "required": ["query"],
167
+ "properties": {
168
+ "query": {
169
+ "type": "string",
170
+ "description": "Search query for tools or collections.",
171
+ },
172
+ "max_results": {
173
+ "type": "integer",
174
+ "description": "Maximum results to return. Default 20.",
175
+ "default": 20,
176
+ },
177
+ },
178
+ },
179
+ ),
180
+ types.Tool(
181
+ name=TOOL_EXECUTE_ACTION,
182
+ description="Execute a specific action with the provided parameters.",
183
+ inputSchema={
184
+ "type": "object",
185
+ "required": ["server_name", "action_name"],
186
+ "properties": {
187
+ "server_name": {
188
+ "type": "string",
189
+ "enum": user_available_servers,
190
+ "description": "The name of the server",
191
+ },
192
+ "action_name": {
193
+ "type": "string",
194
+ "description": "The name of the action/operation to execute",
195
+ },
196
+ "path_params": {
197
+ "type": "string",
198
+ "description": "JSON string containing path parameters",
199
+ },
200
+ "query_params": {
201
+ "type": "string",
202
+ "description": "JSON string containing query parameters",
203
+ },
204
+ "body_schema": {
205
+ "type": "string",
206
+ "description": "JSON string containing request body",
207
+ "default": "{}",
208
+ },
209
+ },
210
+ },
211
+ ),
212
+ types.Tool(
213
+ name=TOOL_SEARCH_DOCUMENTATION,
214
+ description="Search for server action documentations by keyword matching.",
215
+ inputSchema={
216
+ "type": "object",
217
+ "required": ["query", "server_name"],
218
+ "properties": {
219
+ "query": {
220
+ "type": "string",
221
+ "description": "Search keywords",
222
+ },
223
+ "server_name": {
224
+ "type": "string",
225
+ "enum": user_available_servers,
226
+ "description": "Name of the server to search within.",
227
+ },
228
+ "max_results": {
229
+ "type": "integer",
230
+ "description": "Number of results to return. Default: 10",
231
+ "minimum": 1,
232
+ "maximum": 50,
233
+ "default": 10,
234
+ },
235
+ },
236
+ },
237
+ ),
238
+ types.Tool(
239
+ name=TOOL_HANDLE_AUTH_FAILURE,
240
+ description="Handle authentication failures that occur when executing actions.",
241
+ inputSchema={
242
+ "type": "object",
243
+ "required": ["server_name", "intention"],
244
+ "properties": {
245
+ "server_name": {
246
+ "type": "string",
247
+ "enum": user_available_servers,
248
+ "description": "The name of the server",
249
+ },
250
+ "intention": {
251
+ "type": "string",
252
+ "enum": ["get_auth_url", "save_auth_data"],
253
+ "description": "Action to take for authentication",
254
+ },
255
+ "auth_data": {
256
+ "type": "object",
257
+ "description": "Authentication data when saving",
258
+ },
259
+ },
260
+ },
261
+ ),
262
+ ]
263
+
264
+
265
+ async def execute_tool(
266
+ name: str, arguments: dict, client_manager: MCPClientManager
267
+ ) -> List[types.ContentBlock]:
268
+ """Execute a tool with the given arguments."""
269
+ try:
270
+ result = None
271
+
272
+ if name == TOOL_DISCOVER_SERVER_ACTIONS:
273
+ user_query = arguments.get("user_query")
274
+ server_names = arguments.get("server_names")
275
+
276
+ # If no server names provided, use all available servers
277
+ if not server_names:
278
+ server_names = list(client_manager.active_clients.keys())
279
+
280
+ # Discover actions from specified servers
281
+ discovery_result = {}
282
+ for server_name in server_names:
283
+ try:
284
+ client = client_manager.get_client(server_name)
285
+ tools = await client.list_tools()
286
+
287
+ # Filter tools based on user query if provided
288
+ if user_query and tools:
289
+ tools_map = {server_name: tools}
290
+ searcher = UniversalToolSearcher(tools_map)
291
+ search_results = searcher.search(user_query, max_results=50)
292
+
293
+ filtered_action_names = []
294
+ for result_item in search_results:
295
+ for tool in tools:
296
+ if tool["name"] == result_item["name"]:
297
+ filtered_action_names.append(tool)
298
+ break
299
+ discovery_result[server_name] = filtered_action_names
300
+ else:
301
+ discovery_result[server_name] = tools
302
+
303
+ except KeyError:
304
+ discovery_result[server_name] = {
305
+ "error": f"Server '{server_name}' not found or not connected"
306
+ }
307
+ except Exception as e:
308
+ logger.error(f"Error discovering tools for {server_name}: {e}")
309
+ discovery_result[server_name] = {"error": str(e)}
310
+
311
+ result = discovery_result
312
+
313
+ elif name == TOOL_MANAGE_SERVERS:
314
+ list_configured = arguments.get("list_configured_mcps")
315
+ list_sets = arguments.get("list_sets")
316
+
317
+ connect_server = arguments.get("connect")
318
+ connect_set = arguments.get("connect_set")
319
+
320
+ upsert_set = arguments.get("upsert_set")
321
+ delete_set = arguments.get("delete_set")
322
+
323
+ disconnect_server = arguments.get("disconnect")
324
+ disconnect_set = arguments.get("disconnect_set")
325
+
326
+ disconnect_all = arguments.get("disconnect_all")
327
+
328
+ results = []
329
+
330
+ if list_configured:
331
+ active = client_manager.list_active_servers()
332
+ configured = client_manager.server_list.list_servers()
333
+ lines = [f"{s.name}, {'on' if s.name in active else 'off'}" for s in configured]
334
+ results.append("\n".join(lines))
335
+
336
+ if list_sets:
337
+ sets = client_manager.server_list.list_sets()
338
+ lines = []
339
+ for name, data in sets.items():
340
+ desc = data.get('description', '')
341
+ servers = data.get('servers', [])
342
+ includes = data.get('include_sets', [])
343
+ line = f"{name}: {desc}" if desc else f"{name}:"
344
+ if servers:
345
+ line += f"\n servers: {', '.join(servers)}"
346
+ if includes:
347
+ line += f"\n includes: {', '.join(includes)}"
348
+ lines.append(line)
349
+ results.append("\n".join(lines) if lines else "No sets configured")
350
+
351
+ search_sets_query = arguments.get("search_sets")
352
+ if search_sets_query:
353
+ sets = client_manager.server_list.list_sets()
354
+ query_lower = search_sets_query.lower()
355
+ matches = []
356
+ for name, data in sets.items():
357
+ desc = data.get('description', '')
358
+ if query_lower in name.lower() or query_lower in desc.lower():
359
+ servers = ', '.join(data.get('servers', []))
360
+ matches.append(f"{name}: {desc}\n {servers}" if desc else f"{name}:\n {servers}")
361
+ results.append("\n".join(matches) if matches else f"no sets matching '{search_sets_query}'")
362
+
363
+ if upsert_set:
364
+ try:
365
+ name = upsert_set.get("name")
366
+ servers = upsert_set.get("servers", [])
367
+ desc = upsert_set.get("description", "")
368
+ include_sets = upsert_set.get("include_sets")
369
+ if name and (servers or include_sets):
370
+ client_manager.server_list.add_set(name, servers, desc, include_sets)
371
+ results.append(f"set '{name}' saved")
372
+ else:
373
+ results.append("error: missing name, or need servers or include_sets")
374
+ except Exception as e:
375
+ results.append(f"error: {e}")
376
+
377
+ if delete_set:
378
+ success = client_manager.server_list.remove_set(delete_set)
379
+ results.append(f"set '{delete_set}' deleted" if success else f"error: set '{delete_set}' not found")
380
+
381
+ if connect_server:
382
+ server_config = client_manager.server_list.get_server(connect_server)
383
+ if server_config:
384
+ asyncio.create_task(client_manager._connect_server(server_config))
385
+ results.append(f"{connect_server} starting")
386
+ else:
387
+ results.append(f"error: {connect_server} not configured")
388
+
389
+ if connect_set:
390
+ servers_in_set = client_manager.server_list.get_set(connect_set)
391
+ if servers_in_set:
392
+ # Exclusive mode: disconnect everything else first
393
+ connect_set_exclusive = arguments.get("connect_set_exclusive")
394
+ disconnected = []
395
+ if connect_set_exclusive:
396
+ for srv in list(client_manager.active_clients.keys()):
397
+ if srv not in servers_in_set:
398
+ await client_manager._disconnect_server(srv)
399
+ disconnected.append(srv)
400
+
401
+ statuses = []
402
+ for srv in servers_in_set:
403
+ server_config = client_manager.server_list.get_server(srv)
404
+ if server_config and srv not in client_manager.active_clients:
405
+ asyncio.create_task(client_manager._connect_server(server_config))
406
+ statuses.append(f"{srv}: starting")
407
+ elif srv in client_manager.active_clients:
408
+ statuses.append(f"{srv}: on")
409
+ else:
410
+ statuses.append(f"{srv}: not configured")
411
+ prefix = f"connect_set '{connect_set}' (exclusive):" if connect_set_exclusive else f"connect_set '{connect_set}':"
412
+ output = f"{prefix}\n" + "\n".join(statuses)
413
+ if disconnected:
414
+ output += f"\nstopped: {', '.join(disconnected)}"
415
+ results.append(output)
416
+ else:
417
+ results.append(f"error: set '{connect_set}' not found")
418
+
419
+ if disconnect_server:
420
+ await client_manager._disconnect_server(disconnect_server)
421
+ results.append(f"{disconnect_server} off")
422
+
423
+ if disconnect_set:
424
+ servers_in_set = client_manager.server_list.get_set(disconnect_set)
425
+ if servers_in_set:
426
+ for srv in servers_in_set:
427
+ await client_manager._disconnect_server(srv)
428
+ results.append(f"disconnect_set '{disconnect_set}': {len(servers_in_set)} stopped")
429
+ else:
430
+ results.append(f"error: set '{disconnect_set}' not found")
431
+
432
+ if disconnect_all:
433
+ await client_manager.disconnect_all()
434
+ results.append("all disconnected")
435
+
436
+ populate_catalog = arguments.get("populate_catalog")
437
+ if populate_catalog:
438
+ enabled_servers = client_manager.server_list.list_servers(enabled_only=True)
439
+ already_cached = [s for s in enabled_servers if client_manager.catalog.get_tools(s.name)]
440
+ to_populate = [s for s in enabled_servers if not client_manager.catalog.get_tools(s.name)]
441
+
442
+ if not to_populate:
443
+ results.append(f"catalog: {len(already_cached)}/{len(enabled_servers)} cached, nothing to populate")
444
+ else:
445
+ indexed = []
446
+ for server in to_populate:
447
+ try:
448
+ was_connected = server.name in client_manager.active_clients
449
+ if not was_connected:
450
+ await client_manager._connect_server(server)
451
+ client = client_manager.get_client(server.name)
452
+ tools = await client.list_tools()
453
+ client_manager.catalog.update_server(server.name, tools)
454
+ if not was_connected:
455
+ await client_manager._disconnect_server(server.name)
456
+ indexed.append(f"{server.name}: {len(tools)} tools")
457
+ except Exception as e:
458
+ indexed.append(f"{server.name}: error - {e}")
459
+ results.append(f"catalog: indexed {len(to_populate)}, skipped {len(already_cached)}\n" + "\n".join(indexed))
460
+
461
+ return [types.TextContent(type="text", text="\n".join(str(r) for r in results))]
462
+
463
+ elif name == TOOL_SEARCH_MCP_CATALOG:
464
+ query = arguments.get("query")
465
+ max_results = arguments.get("max_results", 20)
466
+
467
+ # Search tools
468
+ tool_results = client_manager.catalog.search(query, max_results)
469
+ # Annotate with current status
470
+ active_servers = client_manager.list_active_servers()
471
+ for r in tool_results:
472
+ r["current_status"] = "online" if r.get("category_name") in active_servers else "offline"
473
+
474
+ # Simple "Collection Search" logic: Check if query matches any Set names or descriptions
475
+ sets = client_manager.server_list.list_sets()
476
+ matching_sets = []
477
+ for set_name, set_data in sets.items():
478
+ # list_sets now returns dict with keys "description" (str) and "servers" (list)
479
+ description = set_data.get("description", "")
480
+
481
+ # BM25-ish: simple containment for now
482
+ if (query.lower() in set_name.lower()) or (query.lower() in description.lower()):
483
+ matching_sets.append({
484
+ "type": "collection",
485
+ "name": set_name,
486
+ "description": description,
487
+ "servers": set_data.get("servers", []),
488
+ "status": "available"
489
+ })
490
+
491
+ final_results = {"collections": matching_sets, "tools": tool_results}
492
+
493
+ return [types.TextContent(type="text", text=json.dumps(final_results, indent=2))]
494
+
495
+ elif name == TOOL_GET_ACTION_DETAILS:
496
+ server_name = arguments.get("server_name")
497
+ action_name = arguments.get("action_name")
498
+
499
+ try:
500
+ client = client_manager.get_client(server_name)
501
+ tools = await client.list_tools()
502
+
503
+ tool = next((t for t in tools if t["name"] == action_name), None)
504
+
505
+ if tool:
506
+ result = {
507
+ "name": tool["name"],
508
+ "description": tool.get("description"),
509
+ "inputSchema": tool.get("inputSchema"),
510
+ }
511
+ else:
512
+ result = {
513
+ "error": f"Action '{action_name}' not found on server '{server_name}'"
514
+ }
515
+ except KeyError:
516
+ result = {
517
+ "error": f"Server '{server_name}' not found or not connected"
518
+ }
519
+
520
+ elif name == TOOL_EXECUTE_ACTION:
521
+ server_name = arguments.get("server_name")
522
+ action_name = arguments.get("action_name")
523
+ path_params = arguments.get("path_params")
524
+ query_params = arguments.get("query_params")
525
+ body_schema = arguments.get("body_schema")
526
+
527
+ # Check if server is connected (no JIT - explicit connect required)
528
+ if server_name not in client_manager.active_clients:
529
+ server_config = client_manager.server_list.get_server(server_name)
530
+ if server_config:
531
+ return [types.TextContent(type="text", text=json.dumps({
532
+ "error": f"Server '{server_name}' is not connected",
533
+ "suggestion": f"Connect first with: manage_servers.exec {{\"connect\": \"{server_name}\"}}"
534
+ }, indent=2))]
535
+ else:
536
+ return [types.TextContent(type="text", text=json.dumps({"error": f"Server '{server_name}' not configured"}, indent=2))]
537
+
538
+
539
+ if not server_name or not action_name:
540
+ return [
541
+ types.TextContent(
542
+ type="text",
543
+ text="Error: Both server_name and action_name are required",
544
+ )
545
+ ]
546
+
547
+ try:
548
+ # Check if server exists and is connected
549
+ if server_name not in client_manager.active_clients:
550
+ available_servers = client_manager.list_active_servers()
551
+ result = _build_error_response(
552
+ f"Server '{server_name}' not found or not connected",
553
+ server_name=server_name,
554
+ available_servers=available_servers,
555
+ suggestion=f"Available servers: {', '.join(available_servers)}" if available_servers else "No servers connected"
556
+ )
557
+ else:
558
+ client = client_manager.get_client(server_name)
559
+
560
+ # Check connection status
561
+ if not client.is_connected():
562
+ result = _build_error_response(
563
+ f"Server '{server_name}' is not connected",
564
+ server_name=server_name,
565
+ suggestion="Try reconnecting the server or check its configuration"
566
+ )
567
+ else:
568
+ action_params = {}
569
+
570
+ # Parse parameters if they're JSON strings
571
+ for param_name, param_value in [
572
+ ("path_params", path_params),
573
+ ("query_params", query_params),
574
+ ("body_schema", body_schema),
575
+ ]:
576
+ if param_value and param_value != "{}":
577
+ try:
578
+ if isinstance(param_value, str):
579
+ action_params.update(json.loads(param_value))
580
+ else:
581
+ action_params.update(param_value)
582
+ except json.JSONDecodeError as json_err:
583
+ result = _build_error_response(
584
+ f"Invalid JSON in {param_name}: {str(json_err)}",
585
+ param_name=param_name,
586
+ param_value=param_value[:100] if isinstance(param_value, str) else str(param_value)[:100]
587
+ )
588
+ break
589
+ else:
590
+ # All parameters parsed successfully - call the tool
591
+ try:
592
+ return await client.call_tool(action_name, action_params)
593
+ except RuntimeError as runtime_err:
594
+ # MCP SDK errors (tool execution failures, connection issues)
595
+ logger.error(f"MCP tool execution error: {str(runtime_err)}")
596
+ logger.error(f"Traceback: {traceback.format_exc()}")
597
+ result = _build_error_response(
598
+ f"Tool '{action_name}' execution failed: {str(runtime_err)}",
599
+ traceback.format_exc(),
600
+ server_name=server_name,
601
+ action_name=action_name,
602
+ suggestion="Check tool parameters and server logs"
603
+ )
604
+ except Exception as tool_err:
605
+ # Unexpected errors during tool execution
606
+ logger.error(f"Unexpected error calling tool: {str(tool_err)}")
607
+ logger.error(f"Traceback: {traceback.format_exc()}")
608
+ result = _build_error_response(
609
+ f"Unexpected error executing '{action_name}': {str(tool_err)}",
610
+ traceback.format_exc(),
611
+ server_name=server_name,
612
+ action_name=action_name
613
+ )
614
+
615
+ except Exception as e:
616
+ # Top-level unexpected errors
617
+ logger.exception(f"Unexpected error in execute_action: {e}")
618
+ result = _build_error_response(
619
+ f"Internal error: {str(e)}",
620
+ traceback.format_exc(),
621
+ server_name=server_name,
622
+ action_name=action_name
623
+ )
624
+
625
+ elif name == TOOL_SEARCH_DOCUMENTATION:
626
+ query = arguments.get("query")
627
+ server_name = arguments.get("server_name")
628
+ max_results = arguments.get("max_results", 10)
629
+
630
+ if not query or not server_name:
631
+ return [
632
+ types.TextContent(
633
+ type="text",
634
+ text="Error: Both query and server_name are required",
635
+ )
636
+ ]
637
+
638
+ try:
639
+ client = client_manager.get_client(server_name)
640
+ tools = await client.list_tools()
641
+
642
+ tools_map = {server_name: tools if tools else []}
643
+ searcher = UniversalToolSearcher(tools_map)
644
+ result = searcher.search(query, max_results=max_results)
645
+ except KeyError:
646
+ result = [
647
+ {"error": f"Server '{server_name}' not found or not connected"}
648
+ ]
649
+ except Exception as e:
650
+ logger.error(f"Error searching documentation: {str(e)}")
651
+ result = [{"error": f"Error searching documentation: {str(e)}"}]
652
+
653
+ elif name == TOOL_HANDLE_AUTH_FAILURE:
654
+ server_name = arguments.get("server_name")
655
+ intention = arguments.get("intention")
656
+ auth_data = arguments.get("auth_data")
657
+
658
+ if not server_name or not intention:
659
+ return [
660
+ types.TextContent(
661
+ type="text",
662
+ text="Error: Both server_name and intention are required",
663
+ )
664
+ ]
665
+
666
+ try:
667
+ if intention == "get_auth_url":
668
+ result = {
669
+ "server": server_name,
670
+ "message": f"Authentication required for server '{server_name}'",
671
+ "instructions": "Please provide authentication credentials",
672
+ "required_fields": {"token": "Authentication token or API key"},
673
+ }
674
+ elif intention == "save_auth_data":
675
+ if not auth_data:
676
+ return [
677
+ types.TextContent(
678
+ type="text",
679
+ text="Error: auth_data is required when intention is 'save_auth_data'",
680
+ )
681
+ ]
682
+ result = {
683
+ "server": server_name,
684
+ "status": "success",
685
+ "message": f"Authentication data saved for server '{server_name}'",
686
+ }
687
+ else:
688
+ result = {"error": f"Invalid intention: '{intention}'"}
689
+ except Exception as e:
690
+ logger.error(f"Error handling auth failure: {str(e)}")
691
+ result = {"error": f"Error handling auth failure: {str(e)}"}
692
+
693
+ else:
694
+ return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
695
+
696
+ # Convert result to TextContent
697
+ return [
698
+ types.TextContent(
699
+ type="text",
700
+ text=(
701
+ json.dumps(result, separators=(",", ":"))
702
+ if isinstance(result, (dict, list))
703
+ else str(result)
704
+ ),
705
+ )
706
+ ]
707
+
708
+ except Exception as e:
709
+ logger.exception(f"Error executing tool {name}: {e}")
710
+ return [
711
+ types.TextContent(
712
+ type="text", text=f"Error executing tool '{name}': {str(e)}"
713
+ )
714
+ ]