Skip to content

Commit 71e596c

Browse files
committed
refactor: add global exception handler
1 parent 2b4e0e1 commit 71e596c

File tree

7 files changed

+95
-60
lines changed

7 files changed

+95
-60
lines changed

src/elasticsearch_mcp_server/server.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ def _get_es_config(self):
4141
def _create_elasticsearch_client(self) -> Elasticsearch:
4242
"""Create and return an Elasticsearch client using configuration from environment."""
4343
config = self._get_es_config()
44-
45-
# Disable SSL warnings
46-
warnings.filterwarnings("ignore", message=".*TLS with verify_certs=False is insecure.*",)
47-
44+
4845
return Elasticsearch(
4946
config["host"],
5047
basic_auth=(config["username"], config["password"]),
+6-13
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
from fastmcp import FastMCP
2-
from mcp.types import TextContent
2+
from typing import Dict, List
3+
from elasticsearch_mcp_server.tools.base import handle_es_exceptions
34

45
class AliasTools:
56
def register_tools(self, mcp: FastMCP):
67
@mcp.tool()
7-
async def list_aliases() -> list[TextContent]:
8+
async def list_aliases() -> List[Dict]:
89
"""List all aliases in the Elasticsearch cluster."""
9-
try:
10-
return self.es_client.cat.aliases(format="json")
11-
except Exception as e:
12-
self.logger.error(f"Error listing aliases: {e}")
13-
return [TextContent(type="text", text=f"Error: {str(e)}")]
10+
return self.es_client.cat.aliases()
1411

1512
@mcp.tool()
16-
async def get_alias(index: str) -> list[TextContent]:
13+
async def get_alias(index: str) -> Dict:
1714
"""
1815
Get alias information for a specific index.
1916
2017
Args:
2118
index: Name of the index
2219
"""
23-
try:
24-
return self.es_client.indices.get_alias(index=index)
25-
except Exception as e:
26-
self.logger.error(f"Error getting alias information: {e}")
27-
return [TextContent(type="text", text=f"Error: {str(e)}")]
20+
return self.es_client.indices.get_alias(index=index)
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import functools
2+
import inspect
3+
import logging
4+
from typing import Callable, TypeVar
5+
from mcp.types import TextContent
6+
7+
# Type variable for function return type
8+
T = TypeVar('T')
9+
10+
def handle_es_exceptions(func: Callable[..., T]) -> Callable[..., list[TextContent]]:
11+
"""
12+
Decorator to handle Elasticsearch exceptions in tool methods.
13+
Logs the error and returns it as a TextContent object.
14+
"""
15+
@functools.wraps(func)
16+
async def wrapper(*args, **kwargs):
17+
logger = logging.getLogger()
18+
try:
19+
return await func(*args, **kwargs)
20+
except Exception as e:
21+
logger.error(f"Unexpected error in {func.__name__}: {e}")
22+
return [TextContent(type="text", text=f"Unexpected error in {func.__name__}: {str(e)}")]
23+
24+
return wrapper
+6-13
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
from fastmcp import FastMCP
2-
from mcp.types import TextContent
2+
from typing import Dict
3+
from elasticsearch_mcp_server.tools.base import handle_es_exceptions
34

45
class ClusterTools:
56
def register_tools(self, mcp: FastMCP):
67
"""Register cluster-related tools."""
78

89
@mcp.tool()
9-
async def get_cluster_health() -> list[TextContent]:
10+
async def get_cluster_health() -> Dict:
1011
"""
1112
Get health status of the Elasticsearch cluster.
1213
Returns information about the number of nodes, shards, etc.
1314
"""
14-
try:
15-
return self.es_client.cluster.health()
16-
except Exception as e:
17-
self.logger.error(f"Error getting cluster health: {e}")
18-
return [TextContent(type="text", text=f"Error: {str(e)}")]
15+
return self.es_client.cluster.health()
1916

2017
@mcp.tool()
21-
async def get_cluster_stats() -> list[TextContent]:
18+
async def get_cluster_stats() -> Dict:
2219
"""
2320
Get statistics from a cluster wide perspective.
2421
The API returns basic index metrics (shard numbers, store size, memory usage) and information
2522
about the current nodes that form the cluster (number, roles, os, jvm versions, memory usage, cpu and installed plugins).
2623
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/cluster-stats.html
2724
"""
28-
try:
29-
return self.es_client.cluster.stats()
30-
except Exception as e:
31-
self.logger.error(f"Error getting cluster stats: {e}")
32-
return [TextContent(type="text", text=f"Error: {str(e)}")]
25+
return self.es_client.cluster.stats()
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
from fastmcp import FastMCP
2-
from mcp.types import TextContent
2+
from typing import Dict
3+
from elasticsearch_mcp_server.tools.base import handle_es_exceptions
34

45
class DocumentTools:
56
def register_tools(self, mcp: FastMCP):
67
"""Register document-related tools."""
78

89
@mcp.tool()
9-
async def search_documents(index: str, body: dict) -> list[TextContent]:
10+
async def search_documents(index: str, body: dict) -> Dict:
1011
"""
1112
Search documents in a specified index using a custom query.
1213
1314
Args:
1415
index: Name of the index to search
1516
body: Elasticsearch query DSL
1617
"""
17-
self.logger.info(f"Searching in index: {index} with query: {body}")
18-
try:
19-
return self.es_client.search(index=index, body=body)
20-
except Exception as e:
21-
self.logger.error(f"Error searching documents: {e}")
22-
return [TextContent(type="text", text=f"Error: {str(e)}")]
18+
return self.es_client.search(index=index, body=body)
+6-18
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,30 @@
11
from fastmcp import FastMCP
2-
from mcp.types import TextContent
32
from typing import Dict
3+
from elasticsearch_mcp_server.tools.base import handle_es_exceptions
44

55
class IndexTools:
66
def register_tools(self, mcp: FastMCP):
77
@mcp.tool()
88
async def list_indices() -> Dict:
99
"""List all indices."""
10-
try:
11-
return self.es_client.cat.indices()
12-
except Exception as e:
13-
self.logger.error(f"Error listing indices: {e}")
14-
return [TextContent(type="text", text=f"Error: {str(e)}")]
10+
return self.es_client.cat.indices()
1511

1612
@mcp.tool()
17-
async def get_mapping(index: str) -> list[TextContent]:
13+
async def get_mapping(index: str) -> Dict:
1814
"""
1915
Get the mapping for an index.
2016
2117
Args:
2218
index: Name of the index
2319
"""
24-
try:
25-
return self.es_client.indices.get_mapping(index=index)
26-
except Exception as e:
27-
self.logger.error(f"Error getting mapping: {e}")
28-
return [TextContent(type="text", text=f"Error: {str(e)}")]
20+
return self.es_client.indices.get_mapping(index=index)
2921

3022
@mcp.tool()
31-
async def get_settings(index: str) -> list[TextContent]:
23+
async def get_settings(index: str) -> Dict:
3224
"""
3325
Get the settings for an index.
3426
3527
Args:
3628
index: Name of the index
3729
"""
38-
try:
39-
return self.es_client.indices.get_settings(index=index)
40-
except Exception as e:
41-
self.logger.error(f"Error getting settings: {e}")
42-
return [TextContent(type="text", text=f"Error: {str(e)}")]
30+
return self.es_client.indices.get_settings(index=index)

src/elasticsearch_mcp_server/tools/register.py

+48-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
11
import logging
2-
2+
import functools
33
from fastmcp import FastMCP
4-
from typing import List, Type
4+
from typing import List, Type, Callable, TypeVar
55
from elasticsearch import Elasticsearch
6+
from elasticsearch_mcp_server.tools.base import handle_es_exceptions
7+
8+
T = TypeVar('T')
9+
10+
def with_exception_handling(tool_instance: object, mcp: FastMCP) -> None:
11+
"""
12+
Register tools from a tool instance with automatic exception handling applied to all tools.
13+
14+
This function temporarily replaces mcp.tool with a wrapped version that automatically
15+
applies the handle_es_exceptions decorator to all registered tool methods.
16+
17+
Args:
18+
tool_instance: The tool instance that has a register_tools method
19+
mcp: The FastMCP instance used for tool registration
20+
"""
21+
# Save the original tool method
22+
original_tool = mcp.tool
23+
24+
@functools.wraps(original_tool)
25+
def wrapped_tool(*args, **kwargs):
26+
# Get the original decorator
27+
decorator = original_tool(*args, **kwargs)
28+
29+
# Return a new decorator that applies both the exception handler and original decorator
30+
def combined_decorator(func):
31+
# First apply the exception handling decorator
32+
wrapped_func = handle_es_exceptions(func)
33+
# Then apply the original mcp.tool decorator
34+
return decorator(wrapped_func)
35+
36+
return combined_decorator
37+
38+
try:
39+
# Temporarily replace mcp.tool with our wrapped version
40+
mcp.tool = wrapped_tool
41+
42+
# Call the registration method on the tool instance
43+
tool_instance.register_tools(mcp)
44+
finally:
45+
# Restore the original mcp.tool to avoid affecting other code that might use mcp.tool
46+
# This ensures that our modification is isolated to just this tool registration
47+
# and prevents multiple nested decorators if register_all_tools is called multiple times
48+
mcp.tool = original_tool
49+
650

751
class ToolsRegister:
852
"""
@@ -39,7 +83,7 @@ def register_all_tools(self, tool_classes: List[Type]):
3983
# Pass the raw Elasticsearch client instead of the wrapper
4084
tool_instance.es_client = self.es_client
4185

42-
# Register the tools from this instance
43-
tool_instance.register_tools(self.mcp)
86+
# Register tools with automatic exception handling
87+
with_exception_handling(tool_instance, self.mcp)
4488

4589
self.logger.info(f"Registered tools from {tool_class.__name__}")

0 commit comments

Comments
 (0)