rootly-mcp-server 2.0.6__py3-none-any.whl → 2.0.8__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.
@@ -7,21 +7,18 @@ the Rootly API's OpenAPI (Swagger) specification using FastMCP's OpenAPI integra
7
7
 
8
8
  import json
9
9
  import os
10
- import re
11
10
  import logging
11
+ from copy import deepcopy
12
12
  from pathlib import Path
13
13
  import requests
14
14
  import httpx
15
- from typing import Any, Dict, List, Optional, Tuple, Union, Callable, Annotated, Literal
16
- from enum import Enum
15
+ from typing import Any, Dict, List, Optional, Annotated
17
16
 
18
17
  from fastmcp import FastMCP
19
18
 
20
- from fastmcp.server.dependencies import get_http_request
21
- from starlette.requests import Request
22
- from pydantic import BaseModel, Field
19
+ from pydantic import Field
23
20
 
24
- from .client import RootlyClient
21
+ from .utils import sanitize_parameters_in_spec
25
22
 
26
23
  # Set up logger
27
24
  logger = logging.getLogger(__name__)
@@ -29,29 +26,68 @@ logger = logging.getLogger(__name__)
29
26
  # Default Swagger URL
30
27
  SWAGGER_URL = "https://rootly-heroku.s3.amazonaws.com/swagger/v1/swagger.json"
31
28
 
29
+ # Default allowed API paths
30
+ DEFAULT_ALLOWED_PATHS = [
31
+ "/incidents/{incident_id}/alerts",
32
+ "/alerts",
33
+ "/alerts/{alert_id}",
34
+ "/severities",
35
+ "/severities/{severity_id}",
36
+ "/teams",
37
+ "/teams/{team_id}",
38
+ "/services",
39
+ "/services/{service_id}",
40
+ "/functionalities",
41
+ "/functionalities/{functionality_id}",
42
+ # Incident types
43
+ "/incident_types",
44
+ "/incident_types/{incident_type_id}",
45
+ # Action items (all, by id, by incident)
46
+ "/incident_action_items",
47
+ "/incident_action_items/{incident_action_item_id}",
48
+ "/incidents/{incident_id}/action_items",
49
+ # Workflows
50
+ "/workflows",
51
+ "/workflows/{workflow_id}",
52
+ # Workflow runs
53
+ "/workflow_runs",
54
+ "/workflow_runs/{workflow_run_id}",
55
+ # Environments
56
+ "/environments",
57
+ "/environments/{environment_id}",
58
+ # Users
59
+ "/users",
60
+ "/users/{user_id}",
61
+ "/users/me",
62
+ # Status pages
63
+ "/status_pages",
64
+ "/status_pages/{status_page_id}",
65
+ ]
66
+
32
67
 
33
68
  class AuthenticatedHTTPXClient:
34
- """An HTTPX client wrapper that handles Rootly API authentication."""
35
-
36
- def __init__(self, base_url: str = "https://api.rootly.com", hosted: bool = False):
69
+ """An HTTPX client wrapper that handles Rootly API authentication and parameter transformation."""
70
+
71
+ def __init__(self, base_url: str = "https://api.rootly.com", hosted: bool = False, parameter_mapping: Optional[Dict[str, str]] = None):
37
72
  self.base_url = base_url
38
73
  self.hosted = hosted
39
74
  self._api_token = None
40
-
75
+ self.parameter_mapping = parameter_mapping or {}
76
+
41
77
  if not self.hosted:
42
78
  self._api_token = self._get_api_token()
43
-
79
+
44
80
  # Create the HTTPX client
45
- headers = {"Content-Type": "application/json", "Accept": "application/json"}
81
+ headers = {"Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json"}
46
82
  if self._api_token:
47
83
  headers["Authorization"] = f"Bearer {self._api_token}"
48
-
84
+
49
85
  self.client = httpx.AsyncClient(
50
86
  base_url=base_url,
51
87
  headers=headers,
52
88
  timeout=30.0
53
89
  )
54
-
90
+
55
91
  def _get_api_token(self) -> Optional[str]:
56
92
  """Get the API token from environment variables."""
57
93
  api_token = os.getenv("ROOTLY_API_TOKEN")
@@ -59,13 +95,56 @@ class AuthenticatedHTTPXClient:
59
95
  logger.warning("ROOTLY_API_TOKEN environment variable is not set")
60
96
  return None
61
97
  return api_token
62
-
98
+
99
+ def _transform_params(self, params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
100
+ """Transform sanitized parameter names back to original names."""
101
+ if not params or not self.parameter_mapping:
102
+ return params
103
+
104
+ transformed = {}
105
+ for key, value in params.items():
106
+ # Use the original name if we have a mapping, otherwise keep the sanitized name
107
+ original_key = self.parameter_mapping.get(key, key)
108
+ transformed[original_key] = value
109
+ if original_key != key:
110
+ logger.debug(f"Transformed parameter: '{key}' -> '{original_key}'")
111
+ return transformed
112
+
113
+ async def request(self, method: str, url: str, **kwargs):
114
+ """Override request to transform parameters."""
115
+ # Transform query parameters
116
+ if 'params' in kwargs:
117
+ kwargs['params'] = self._transform_params(kwargs['params'])
118
+
119
+ # Call the underlying client's request method
120
+ return await self.client.request(method, url, **kwargs)
121
+
122
+ async def get(self, url: str, **kwargs):
123
+ """Proxy to request with GET method."""
124
+ return await self.request('GET', url, **kwargs)
125
+
126
+ async def post(self, url: str, **kwargs):
127
+ """Proxy to request with POST method."""
128
+ return await self.request('POST', url, **kwargs)
129
+
130
+ async def put(self, url: str, **kwargs):
131
+ """Proxy to request with PUT method."""
132
+ return await self.request('PUT', url, **kwargs)
133
+
134
+ async def patch(self, url: str, **kwargs):
135
+ """Proxy to request with PATCH method."""
136
+ return await self.request('PATCH', url, **kwargs)
137
+
138
+ async def delete(self, url: str, **kwargs):
139
+ """Proxy to request with DELETE method."""
140
+ return await self.request('DELETE', url, **kwargs)
141
+
63
142
  async def __aenter__(self):
64
- return self.client
65
-
143
+ return self
144
+
66
145
  async def __aexit__(self, exc_type, exc_val, exc_tb):
67
- await self.client.aclose()
68
-
146
+ pass
147
+
69
148
  def __getattr__(self, name):
70
149
  # Delegate all other attributes to the underlying client
71
150
  return getattr(self.client, name)
@@ -93,44 +172,8 @@ def create_rootly_mcp_server(
93
172
  """
94
173
  # Set default allowed paths if none provided
95
174
  if allowed_paths is None:
96
- allowed_paths = [
97
- "/incidents",
98
- "/incidents/{incident_id}/alerts",
99
- "/alerts",
100
- "/alerts/{alert_id}",
101
- "/severities",
102
- "/severities/{severity_id}",
103
- "/teams",
104
- "/teams/{team_id}",
105
- "/services",
106
- "/services/{service_id}",
107
- "/functionalities",
108
- "/functionalities/{functionality_id}",
109
- # Incident types
110
- "/incident_types",
111
- "/incident_types/{incident_type_id}",
112
- # Action items (all, by id, by incident)
113
- "/incident_action_items",
114
- "/incident_action_items/{incident_action_item_id}",
115
- "/incidents/{incident_id}/action_items",
116
- # Workflows
117
- "/workflows",
118
- "/workflows/{workflow_id}",
119
- # Workflow runs
120
- "/workflow_runs",
121
- "/workflow_runs/{workflow_run_id}",
122
- # Environments
123
- "/environments",
124
- "/environments/{environment_id}",
125
- # Users
126
- "/users",
127
- "/users/{user_id}",
128
- "/users/me",
129
- # Status pages
130
- "/status_pages",
131
- "/status_pages/{status_page_id}",
132
- ]
133
-
175
+ allowed_paths = DEFAULT_ALLOWED_PATHS
176
+
134
177
  # Add /v1 prefix to paths if not present
135
178
  allowed_paths_v1 = [
136
179
  f"/v1{path}" if not path.startswith("/v1") else path
@@ -147,22 +190,23 @@ def create_rootly_mcp_server(
147
190
  filtered_spec = _filter_openapi_spec(swagger_spec, allowed_paths_v1)
148
191
  logger.info(f"Filtered spec to {len(filtered_spec.get('paths', {}))} allowed paths")
149
192
 
193
+ # Sanitize all parameter names in the filtered spec to be MCP-compliant
194
+ parameter_mapping = sanitize_parameters_in_spec(filtered_spec)
195
+ logger.info(f"Sanitized parameter names for MCP compatibility (mapped {len(parameter_mapping)} parameters)")
196
+
150
197
  # Determine the base URL
151
198
  if base_url is None:
152
199
  base_url = os.getenv("ROOTLY_BASE_URL", "https://api.rootly.com")
153
-
200
+
154
201
  logger.info(f"Using Rootly API base URL: {base_url}")
155
202
 
156
- # Create the authenticated HTTP client
157
- try:
158
- http_client = AuthenticatedHTTPXClient(
159
- base_url=base_url,
160
- hosted=hosted
161
- )
162
- except Exception as e:
163
- logger.warning(f"Failed to create authenticated client: {e}")
164
- # Create a mock client for testing
165
- http_client = httpx.AsyncClient(base_url=base_url)
203
+ # Create the authenticated HTTP client with parameter mapping
204
+
205
+ http_client = AuthenticatedHTTPXClient(
206
+ base_url=base_url,
207
+ hosted=hosted,
208
+ parameter_mapping=parameter_mapping
209
+ )
166
210
 
167
211
  # Create the MCP server using OpenAPI integration
168
212
  # By default, all routes become tools which is what we want
@@ -173,10 +217,16 @@ def create_rootly_mcp_server(
173
217
  timeout=30.0,
174
218
  tags={"rootly", "incident-management"},
175
219
  )
176
-
220
+
221
+ @mcp.custom_route("/healthz", methods=["GET"])
222
+ @mcp.custom_route("/health", methods=["GET"])
223
+ async def health_check(request):
224
+ from starlette.responses import PlainTextResponse
225
+ return PlainTextResponse("OK")
226
+
177
227
  # Add some custom tools for enhanced functionality
178
228
  @mcp.tool()
179
- def list_endpoints() -> str:
229
+ def list_endpoints() -> list:
180
230
  """List all available Rootly API endpoints with their descriptions."""
181
231
  endpoints = []
182
232
  for path, path_item in filtered_spec.get("paths", {}).items():
@@ -186,7 +236,7 @@ def create_rootly_mcp_server(
186
236
 
187
237
  summary = operation.get("summary", "")
188
238
  description = operation.get("description", "")
189
-
239
+
190
240
  endpoints.append({
191
241
  "path": path,
192
242
  "method": method.upper(),
@@ -194,106 +244,121 @@ def create_rootly_mcp_server(
194
244
  "description": description,
195
245
  })
196
246
 
197
- return json.dumps(endpoints, indent=2)
247
+ return endpoints
248
+
249
+ async def make_authenticated_request(method: str, url: str, **kwargs):
250
+ """Make an authenticated request, extracting token from MCP headers in hosted mode."""
251
+ # In hosted mode, get token from MCP request headers
252
+ if hosted:
253
+ try:
254
+ from fastmcp.server.dependencies import get_http_headers
255
+ request_headers = get_http_headers()
256
+ auth_header = request_headers.get("authorization", "")
257
+ if auth_header:
258
+ # Add authorization header to the request
259
+ if "headers" not in kwargs:
260
+ kwargs["headers"] = {}
261
+ kwargs["headers"]["Authorization"] = auth_header
262
+ except Exception:
263
+ pass # Fallback to default client behavior
264
+
265
+ # Make the request using the underlying httpx client
266
+ return await http_client.client.request(method, url, **kwargs)
198
267
 
199
268
  @mcp.tool()
200
- async def search_incidents_paginated(
269
+ async def search_incidents(
201
270
  query: Annotated[str, Field(description="Search query to filter incidents by title/summary")] = "",
202
- page_size: Annotated[int, Field(description="Number of results per page (max: 100)", ge=1, le=100)] = 100,
203
- page_number: Annotated[int, Field(description="Page number to retrieve", ge=1)] = 1,
204
- ) -> str:
271
+ page_size: Annotated[int, Field(description="Number of results per page (max: 20)", ge=1, le=20)] = 10,
272
+ page_number: Annotated[int, Field(description="Page number to retrieve (use 0 for all pages)", ge=0)] = 1,
273
+ max_results: Annotated[int, Field(description="Maximum total results when fetching all pages (ignored if page_number > 0)", ge=1, le=100)] = 20,
274
+ ) -> dict:
205
275
  """
206
- Search incidents with enhanced pagination control.
207
-
208
- This tool provides better pagination handling than the standard API endpoint.
276
+ Search incidents with flexible pagination control.
277
+
278
+ Use page_number=0 to fetch all matching results across multiple pages up to max_results.
279
+ Use page_number>0 to fetch a specific page.
209
280
  """
210
- params = {
211
- "page[size]": min(page_size, 100),
212
- "page[number]": page_number,
213
- }
214
- if query:
215
- params["filter[search]"] = query
216
-
217
- try:
218
- async with http_client as client:
219
- response = await client.get("/v1/incidents", params=params)
281
+ # Single page mode
282
+ if page_number > 0:
283
+ params = {
284
+ "page[size]": min(page_size, 20),
285
+ "page[number]": page_number,
286
+ "include": "",
287
+ }
288
+ if query:
289
+ params["filter[search]"] = query
290
+
291
+ try:
292
+ response = await make_authenticated_request("GET", "/v1/incidents", params=params)
220
293
  response.raise_for_status()
221
- result = response.json()
222
- except Exception as e:
223
- result = {"error": str(e)}
224
-
225
- return json.dumps(result, indent=2)
294
+ return response.json()
295
+ except Exception as e:
296
+ return {"error": str(e)}
226
297
 
227
- @mcp.tool()
228
- async def get_all_incidents_matching(
229
- query: Annotated[str, Field(description="Search query to filter incidents by title/summary")] = "",
230
- max_results: Annotated[int, Field(description="Maximum number of results to return", ge=1, le=1000)] = 500,
231
- ) -> str:
232
- """
233
- Get all incidents matching a query by automatically fetching multiple pages.
234
-
235
- This tool automatically handles pagination to fetch multiple pages of results.
236
- """
298
+ # Multi-page mode (page_number = 0)
237
299
  all_incidents = []
238
- page_number = 1
239
- page_size = 100
240
-
300
+ current_page = 1
301
+ effective_page_size = min(page_size, 10)
302
+
241
303
  try:
242
- async with http_client as client:
243
- while len(all_incidents) < max_results:
244
- params = {
245
- "page[size]": page_size,
246
- "page[number]": page_number,
247
- }
248
- if query:
249
- params["filter[search]"] = query
250
-
251
- try:
252
- response = await client.get("/v1/incidents", params=params)
253
- response.raise_for_status()
254
- response_data = response.json()
304
+ while len(all_incidents) < max_results:
305
+ params = {
306
+ "page[size]": effective_page_size,
307
+ "page[number]": current_page,
308
+ "include": "",
309
+ }
310
+ if query:
311
+ params["filter[search]"] = query
312
+
313
+ try:
314
+ response = await make_authenticated_request("GET", "/v1/incidents", params=params)
315
+ response.raise_for_status()
316
+ response_data = response.json()
317
+
318
+ if "data" in response_data:
319
+ incidents = response_data["data"]
320
+ if not incidents:
321
+ break
255
322
 
256
- if "data" in response_data:
257
- incidents = response_data["data"]
258
- if not incidents: # No more results
259
- break
260
- all_incidents.extend(incidents)
261
-
262
- # Check if we have more pages
263
- meta = response_data.get("meta", {})
264
- current_page = meta.get("current_page", page_number)
265
- total_pages = meta.get("total_pages", 1)
266
-
267
- if current_page >= total_pages:
268
- break # No more pages
269
-
270
- page_number += 1
271
- else:
272
- break # Unexpected response format
273
-
274
- except Exception as e:
275
- logger.error(f"Error fetching incidents page {page_number}: {e}")
323
+ all_incidents.extend(incidents)
324
+
325
+ # Check if we have more pages
326
+ meta = response_data.get("meta", {})
327
+ current_page_meta = meta.get("current_page", current_page)
328
+ total_pages = meta.get("total_pages", 1)
329
+
330
+ if current_page_meta >= total_pages:
331
+ break
332
+
333
+ current_page += 1
334
+ else:
276
335
  break
277
-
278
- # Limit to max_results
279
- if len(all_incidents) > max_results:
280
- all_incidents = all_incidents[:max_results]
281
-
282
- result = {
283
- "data": all_incidents,
284
- "meta": {
285
- "total_fetched": len(all_incidents),
286
- "max_results": max_results,
287
- "query": query
288
- }
336
+
337
+ except Exception as e:
338
+ # Re-raise authentication or critical errors
339
+ if "401" in str(e) or "Unauthorized" in str(e) or "authentication" in str(e).lower():
340
+ raise e
341
+ break
342
+
343
+ # Limit to max_results
344
+ if len(all_incidents) > max_results:
345
+ all_incidents = all_incidents[:max_results]
346
+
347
+ return {
348
+ "data": all_incidents,
349
+ "meta": {
350
+ "total_fetched": len(all_incidents),
351
+ "max_results": max_results,
352
+ "query": query,
353
+ "pages_fetched": current_page - 1,
354
+ "page_size": effective_page_size
289
355
  }
356
+ }
290
357
  except Exception as e:
291
- result = {"error": str(e)}
292
-
293
- return json.dumps(result, indent=2)
358
+ return {"error": str(e)}
294
359
 
295
360
  # Log server creation (tool count will be shown when tools are accessed)
296
- logger.info(f"Created Rootly MCP Server successfully")
361
+ logger.info("Created Rootly MCP Server successfully")
297
362
  return mcp
298
363
 
299
364
 
@@ -395,25 +460,26 @@ def _filter_openapi_spec(spec: Dict[str, Any], allowed_paths: List[str]) -> Dict
395
460
  Returns:
396
461
  A filtered OpenAPI specification with cleaned schema references.
397
462
  """
398
- filtered_spec = spec.copy()
399
-
463
+ # Use deepcopy to ensure all nested structures are properly copied
464
+ filtered_spec = deepcopy(spec)
465
+
400
466
  # Filter paths
401
- original_paths = spec.get("paths", {})
467
+ original_paths = filtered_spec.get("paths", {})
402
468
  filtered_paths = {
403
469
  path: path_item
404
470
  for path, path_item in original_paths.items()
405
471
  if path in allowed_paths
406
472
  }
407
-
473
+
408
474
  filtered_spec["paths"] = filtered_paths
409
-
475
+
410
476
  # Clean up schema references that might be broken
411
477
  # Remove problematic schema references from request bodies and parameters
412
478
  for path, path_item in filtered_paths.items():
413
479
  for method, operation in path_item.items():
414
480
  if method.lower() not in ["get", "post", "put", "delete", "patch"]:
415
481
  continue
416
-
482
+
417
483
  # Clean request body schemas
418
484
  if "requestBody" in operation:
419
485
  request_body = operation["requestBody"]
@@ -429,8 +495,21 @@ def _filter_openapi_spec(spec: Dict[str, Any], allowed_paths: List[str]) -> Dict
429
495
  "description": "Request parameters for this endpoint",
430
496
  "additionalProperties": True
431
497
  }
432
-
433
- # Clean parameter schemas
498
+
499
+ # Remove response schemas to avoid validation issues
500
+ # FastMCP will still return the data, just without strict validation
501
+ if "responses" in operation:
502
+ for status_code, response in operation["responses"].items():
503
+ if "content" in response:
504
+ for content_type, content_info in response["content"].items():
505
+ if "schema" in content_info:
506
+ # Replace with a simple schema that accepts any response
507
+ content_info["schema"] = {
508
+ "type": "object",
509
+ "additionalProperties": True
510
+ }
511
+
512
+ # Clean parameter schemas (parameter names are already sanitized)
434
513
  if "parameters" in operation:
435
514
  for param in operation["parameters"]:
436
515
  if "schema" in param and "$ref" in param["schema"]:
@@ -441,7 +520,7 @@ def _filter_openapi_spec(spec: Dict[str, Any], allowed_paths: List[str]) -> Dict
441
520
  "type": "string",
442
521
  "description": param.get("description", "Parameter value")
443
522
  }
444
-
523
+
445
524
  # Also clean up any remaining broken references in components
446
525
  if "components" in filtered_spec and "schemas" in filtered_spec["components"]:
447
526
  schemas = filtered_spec["components"]["schemas"]
@@ -450,11 +529,11 @@ def _filter_openapi_spec(spec: Dict[str, Any], allowed_paths: List[str]) -> Dict
450
529
  for schema_name, schema_def in schemas.items():
451
530
  if isinstance(schema_def, dict) and _has_broken_references(schema_def):
452
531
  schemas_to_remove.append(schema_name)
453
-
532
+
454
533
  for schema_name in schemas_to_remove:
455
534
  logger.warning(f"Removing schema with broken references: {schema_name}")
456
535
  del schemas[schema_name]
457
-
536
+
458
537
  return filtered_spec
459
538
 
460
539
 
@@ -465,13 +544,13 @@ def _has_broken_references(schema_def: Dict[str, Any]) -> bool:
465
544
  # List of known broken references in the Rootly API spec
466
545
  broken_refs = [
467
546
  "incident_trigger_params",
468
- "new_workflow",
547
+ "new_workflow",
469
548
  "update_workflow",
470
549
  "workflow"
471
550
  ]
472
551
  if any(broken_ref in ref_path for broken_ref in broken_refs):
473
552
  return True
474
-
553
+
475
554
  # Recursively check nested schemas
476
555
  for key, value in schema_def.items():
477
556
  if isinstance(value, dict):
@@ -481,7 +560,7 @@ def _has_broken_references(schema_def: Dict[str, Any]) -> bool:
481
560
  for item in value:
482
561
  if isinstance(item, dict) and _has_broken_references(item):
483
562
  return True
484
-
563
+
485
564
  return False
486
565
 
487
566
 
@@ -489,10 +568,10 @@ def _has_broken_references(schema_def: Dict[str, Any]) -> bool:
489
568
  class RootlyMCPServer(FastMCP):
490
569
  """
491
570
  Legacy Rootly MCP Server class for backward compatibility.
492
-
571
+
493
572
  This class is deprecated. Use create_rootly_mcp_server() instead.
494
573
  """
495
-
574
+
496
575
  def __init__(
497
576
  self,
498
577
  swagger_path: Optional[str] = None,
@@ -506,7 +585,7 @@ class RootlyMCPServer(FastMCP):
506
585
  logger.warning(
507
586
  "RootlyMCPServer class is deprecated. Use create_rootly_mcp_server() function instead."
508
587
  )
509
-
588
+
510
589
  # Create the server using the new function
511
590
  server = create_rootly_mcp_server(
512
591
  swagger_path=swagger_path,
@@ -514,7 +593,7 @@ class RootlyMCPServer(FastMCP):
514
593
  allowed_paths=allowed_paths,
515
594
  hosted=hosted
516
595
  )
517
-
596
+
518
597
  # Copy the server's state to this instance
519
598
  super().__init__(name, *args, **kwargs)
520
599
  # For compatibility, store reference to the new server
@@ -33,7 +33,7 @@ async def test_server():
33
33
  hosted=False # Use local API token
34
34
  )
35
35
 
36
- print(f"✅ Server created successfully")
36
+ print("✅ Server created successfully")
37
37
  print(f"Server type: {type(server)}")
38
38
 
39
39
  # Use the get_tools method to access tools
@@ -78,11 +78,11 @@ async def test_server():
78
78
  if props:
79
79
  print(f" Parameters: {', '.join(props.keys())}")
80
80
  else:
81
- print(f"\n⚠️ No tools found")
81
+ print("\n⚠️ No tools found")
82
82
 
83
83
  # Test accessing a specific tool
84
84
  if tool_count > 0:
85
- print(f"\n🔍 Testing tool access...")
85
+ print("\n🔍 Testing tool access...")
86
86
  if isinstance(tools, dict):
87
87
  first_tool_name = tools_names[0]
88
88
  first_tool = tools[first_tool_name]
@@ -93,15 +93,17 @@ async def test_server():
93
93
  print(f" ✅ First tool: {first_tool_name}")
94
94
  print(f" Tool details: {first_tool}")
95
95
 
96
- # Try to get tool by name
96
+ # Try to get tools and find the specific tool
97
97
  try:
98
- retrieved_tool = await server.get_tool(first_tool_name)
99
- if retrieved_tool:
98
+ all_tools = await server.get_tools()
99
+ if first_tool_name in all_tools:
100
+ retrieved_tool = all_tools[first_tool_name]
100
101
  print(f" ✅ Successfully retrieved tool by name: {first_tool_name}")
102
+ print(f" Retrieved tool type: {type(retrieved_tool)}")
101
103
  else:
102
- print(f" ❌ Could not retrieve tool by name: {first_tool_name}")
104
+ print(f" ❌ Could not find tool by name: {first_tool_name}")
103
105
  except Exception as e:
104
- print(f" ❌ Error retrieving tool: {e}")
106
+ print(f" ❌ Error retrieving tools: {e}")
105
107
 
106
108
  except Exception as e:
107
109
  print(f"❌ Error accessing tools: {e}")
@@ -109,7 +111,7 @@ async def test_server():
109
111
  traceback.print_exc()
110
112
  tool_count = 0
111
113
 
112
- print(f"\n🎉 Test completed successfully!")
114
+ print("\n🎉 Test completed successfully!")
113
115
  print(f"Total tools found: {tool_count}")
114
116
 
115
117
  except Exception as e: