elasticsearch-mcp-server 2.0.17__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.
src/risk_config.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Configuration for high-risk operations management.
3
+ """
4
+ import os
5
+ from typing import Set, Dict, Any, Callable
6
+ from functools import wraps
7
+ import logging
8
+
9
+ # Define default high-risk operations per tool class (all write operations)
10
+ HIGH_RISK_OPERATIONS = {
11
+ "IndexTools": {
12
+ "create_index",
13
+ "delete_index",
14
+ },
15
+ "DocumentTools": {
16
+ "index_document",
17
+ "delete_document",
18
+ "delete_by_query",
19
+ },
20
+ "DataStreamTools": {
21
+ "create_data_stream",
22
+ "delete_data_stream",
23
+ },
24
+ "AliasTools": {
25
+ "put_alias",
26
+ "delete_alias",
27
+ },
28
+ "GeneralTools": {
29
+ "general_api_request",
30
+ },
31
+ }
32
+
33
+ class RiskManager:
34
+ """Manages high-risk operation filtering and control."""
35
+
36
+ def __init__(self):
37
+ self.logger = logging.getLogger(__name__)
38
+ self.high_risk_ops_disabled = self._is_high_risk_disabled()
39
+
40
+ if self.high_risk_ops_disabled:
41
+ self.disabled_operations = self._get_disabled_operations()
42
+ self.logger.info("High-risk operations are disabled")
43
+ self.logger.info(f"Disabled operations: {self.disabled_operations}")
44
+ else:
45
+ self.disabled_operations = set()
46
+ self.logger.info("High-risk operations are not disabled")
47
+
48
+ def _is_high_risk_disabled(self) -> bool:
49
+ """Check if high-risk operations should be disabled."""
50
+ return os.environ.get("DISABLE_HIGH_RISK_OPERATIONS", "false").lower() == "true"
51
+
52
+ def _get_disabled_operations(self) -> Set[str]:
53
+ """Get the set of operations that should be disabled."""
54
+ # Check for custom disabled operations list
55
+ custom_ops = os.environ.get("DISABLE_OPERATIONS", "")
56
+ if custom_ops:
57
+ # User provided custom list
58
+ return set(op.strip() for op in custom_ops.split(",") if op.strip())
59
+
60
+ # Use default high-risk operations
61
+ all_ops = set()
62
+ for tool_ops in HIGH_RISK_OPERATIONS.values():
63
+ all_ops.update(tool_ops)
64
+ return all_ops
65
+
66
+ def is_operation_allowed(self, tool_class_name: str, operation_name: str) -> bool:
67
+ """Check if an operation is allowed to be executed."""
68
+ # Only check against the disabled_operations set
69
+ # (which is either custom or default based on environment variables)
70
+ if operation_name in self.disabled_operations:
71
+ self.logger.info(f"Operation '{operation_name}' from {tool_class_name} is disabled")
72
+ return False
73
+
74
+ return True
75
+
76
+ # Global risk manager instance
77
+ risk_manager = RiskManager()
src/server.py ADDED
@@ -0,0 +1,166 @@
1
+ import logging
2
+ import sys
3
+ import argparse
4
+
5
+ from fastmcp import FastMCP
6
+
7
+ from src.clients import create_search_client
8
+ from src.tools.alias import AliasTools
9
+ from src.tools.cluster import ClusterTools
10
+ from src.tools.data_stream import DataStreamTools
11
+ from src.tools.document import DocumentTools
12
+ from src.tools.general import GeneralTools
13
+ from src.tools.index import IndexTools
14
+ from src.tools.register import ToolsRegister
15
+ from src.version import __version__ as VERSION
16
+
17
+ class SearchMCPServer:
18
+ def __init__(self, engine_type):
19
+ # Set engine type
20
+ self.engine_type = engine_type
21
+ self.name = f"{self.engine_type}-mcp-server"
22
+ self.mcp = FastMCP(self.name)
23
+
24
+ # Configure logging
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
28
+ )
29
+ self.logger = logging.getLogger(__name__)
30
+ self.logger.info(f"Initializing {self.name}, Version: {VERSION}")
31
+
32
+ # Create the corresponding search client
33
+ self.search_client = create_search_client(self.engine_type)
34
+
35
+ # Initialize tools
36
+ self._register_tools()
37
+
38
+ def _register_tools(self):
39
+ """Register all MCP tools."""
40
+ # Create a tools register
41
+ register = ToolsRegister(self.logger, self.search_client, self.mcp)
42
+
43
+ # Define all tool classes to register
44
+ tool_classes = [
45
+ IndexTools,
46
+ DocumentTools,
47
+ ClusterTools,
48
+ AliasTools,
49
+ DataStreamTools,
50
+ GeneralTools,
51
+ ]
52
+ # Register all tools
53
+ register.register_all_tools(tool_classes)
54
+
55
+
56
+ def run_search_server(engine_type, transport, host, port, path):
57
+ """Run search server with specified engine type and transport options.
58
+
59
+ Args:
60
+ engine_type: Type of search engine to use ("elasticsearch" or "opensearch")
61
+ transport: Transport protocol to use ("stdio", "streamable-http", or "sse")
62
+ host: Host to bind to when using HTTP transports
63
+ port: Port to bind to when using HTTP transports
64
+ path: URL path prefix for HTTP transports
65
+ """
66
+
67
+ server = SearchMCPServer(engine_type=engine_type)
68
+
69
+ if transport in ["streamable-http", "sse"]:
70
+ server.logger.info(f"Starting {server.name} with {transport} transport on {host}:{port}{path}")
71
+ server.mcp.run(transport=transport, host=host, port=port, path=path)
72
+ else:
73
+ server.logger.info(f"Starting {server.name} with {transport} transport")
74
+ server.mcp.run(transport=transport)
75
+
76
+ def parse_server_args():
77
+ """Parse command line arguments for the MCP server.
78
+
79
+ Returns:
80
+ Parsed arguments
81
+ """
82
+ parser = argparse.ArgumentParser()
83
+ parser.add_argument(
84
+ "--transport", "-t",
85
+ default="stdio",
86
+ choices=["stdio", "streamable-http", "sse"],
87
+ help="Transport protocol to use (default: stdio)"
88
+ )
89
+ parser.add_argument(
90
+ "--host", "-H",
91
+ default="127.0.0.1",
92
+ help="Host to bind to when using HTTP transports (default: 127.0.0.1)"
93
+ )
94
+ parser.add_argument(
95
+ "--port", "-p",
96
+ type=int,
97
+ default=8000,
98
+ help="Port to bind to when using HTTP transports (default: 8000)"
99
+ )
100
+ parser.add_argument(
101
+ "--path", "-P",
102
+ help="URL path prefix for HTTP transports (default: /mcp for streamable-http, /sse for sse)"
103
+ )
104
+
105
+ args = parser.parse_args()
106
+
107
+ # Set default path based on transport type if not specified
108
+ if args.path is None:
109
+ if args.transport == "sse":
110
+ args.path = "/sse"
111
+ else:
112
+ args.path = "/mcp"
113
+
114
+ return args
115
+
116
+ def elasticsearch_mcp_server():
117
+ """Entry point for Elasticsearch MCP server."""
118
+ args = parse_server_args()
119
+
120
+ # Run the server with the specified options
121
+ run_search_server(
122
+ engine_type="elasticsearch",
123
+ transport=args.transport,
124
+ host=args.host,
125
+ port=args.port,
126
+ path=args.path
127
+ )
128
+
129
+ def opensearch_mcp_server():
130
+ """Entry point for OpenSearch MCP server."""
131
+ args = parse_server_args()
132
+
133
+ # Run the server with the specified options
134
+ run_search_server(
135
+ engine_type="opensearch",
136
+ transport=args.transport,
137
+ host=args.host,
138
+ port=args.port,
139
+ path=args.path
140
+ )
141
+
142
+ if __name__ == "__main__":
143
+ # Require elasticsearch-mcp-server or opensearch-mcp-server as the first argument
144
+ if len(sys.argv) <= 1 or sys.argv[1] not in ["elasticsearch-mcp-server", "opensearch-mcp-server"]:
145
+ print("Error: First argument must be 'elasticsearch-mcp-server' or 'opensearch-mcp-server'")
146
+ sys.exit(1)
147
+
148
+ # Determine engine type based on the first argument
149
+ engine_type = "elasticsearch" # Default
150
+ if sys.argv[1] == "opensearch-mcp-server":
151
+ engine_type = "opensearch"
152
+
153
+ # Remove the first argument so it doesn't interfere with argparse
154
+ sys.argv.pop(1)
155
+
156
+ # Parse command line arguments
157
+ args = parse_server_args()
158
+
159
+ # Run the server with the specified options
160
+ run_search_server(
161
+ engine_type=engine_type,
162
+ transport=args.transport,
163
+ host=args.host,
164
+ port=args.port,
165
+ path=args.path
166
+ )
src/tools/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from src.tools.alias import AliasTools
2
+ from src.tools.cluster import ClusterTools
3
+ from src.tools.document import DocumentTools
4
+ from src.tools.general import GeneralTools
5
+ from src.tools.index import IndexTools
6
+ from src.tools.register import ToolsRegister
7
+
8
+ __all__ = [
9
+ 'AliasTools',
10
+ 'ClusterTools',
11
+ 'DocumentTools',
12
+ 'GeneralTools',
13
+ 'IndexTools',
14
+ 'ToolsRegister',
15
+ ]
src/tools/alias.py ADDED
@@ -0,0 +1,46 @@
1
+ from typing import Dict, List
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ class AliasTools:
6
+ def __init__(self, search_client):
7
+ self.search_client = search_client
8
+ def register_tools(self, mcp: FastMCP):
9
+ @mcp.tool()
10
+ def list_aliases() -> List[Dict]:
11
+ """List all aliases."""
12
+ return self.search_client.list_aliases()
13
+
14
+ @mcp.tool()
15
+ def get_alias(index: str) -> Dict:
16
+ """
17
+ Get alias information for a specific index.
18
+
19
+ Args:
20
+ index: Name of the index
21
+ """
22
+ return self.search_client.get_alias(index=index)
23
+
24
+ @mcp.tool()
25
+ def put_alias(index: str, name: str, body: Dict) -> Dict:
26
+ """
27
+ Create or update an alias for a specific index.
28
+
29
+ Args:
30
+ index: Name of the index
31
+ name: Name of the alias
32
+ body: Alias configuration
33
+ """
34
+ return self.search_client.put_alias(index=index, name=name, body=body)
35
+
36
+ @mcp.tool()
37
+ def delete_alias(index: str, name: str) -> Dict:
38
+ """
39
+ Delete an alias for a specific index.
40
+
41
+ Args:
42
+ index: Name of the index
43
+ name: Name of the alias
44
+ """
45
+ return self.search_client.delete_alias(index=index, name=name)
46
+
src/tools/cluster.py ADDED
@@ -0,0 +1,17 @@
1
+ from typing import Dict
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ class ClusterTools:
6
+ def __init__(self, search_client):
7
+ self.search_client = search_client
8
+ def register_tools(self, mcp: FastMCP):
9
+ @mcp.tool()
10
+ def get_cluster_health() -> Dict:
11
+ """Returns basic information about the health of the cluster."""
12
+ return self.search_client.get_cluster_health()
13
+
14
+ @mcp.tool()
15
+ def get_cluster_stats() -> Dict:
16
+ """Returns high-level overview of cluster statistics."""
17
+ return self.search_client.get_cluster_stats()
@@ -0,0 +1,47 @@
1
+ from typing import Dict, Optional
2
+ from fastmcp import FastMCP
3
+
4
+ class DataStreamTools:
5
+ def __init__(self, search_client):
6
+ self.search_client = search_client
7
+
8
+ def register_tools(self, mcp: FastMCP):
9
+ """Register data stream tools with the MCP server."""
10
+
11
+ @mcp.tool()
12
+ def create_data_stream(name: str) -> Dict:
13
+ """Create a new data stream.
14
+
15
+ This creates a new data stream with the specified name.
16
+ The data stream must have a matching index template before creation.
17
+
18
+ Args:
19
+ name: Name of the data stream to create
20
+ """
21
+ return self.search_client.create_data_stream(name=name)
22
+
23
+ @mcp.tool()
24
+ def get_data_stream(name: Optional[str] = None) -> Dict:
25
+ """Get information about one or more data streams.
26
+
27
+ Retrieves configuration, mappings, settings, and other information
28
+ about the specified data streams.
29
+
30
+ Args:
31
+ name: Name of the data stream(s) to retrieve.
32
+ Can be a comma-separated list or wildcard pattern.
33
+ If not provided, retrieves all data streams.
34
+ """
35
+ return self.search_client.get_data_stream(name=name)
36
+
37
+ @mcp.tool()
38
+ def delete_data_stream(name: str) -> Dict:
39
+ """Delete one or more data streams.
40
+
41
+ Permanently deletes the specified data streams and all their backing indices.
42
+
43
+ Args:
44
+ name: Name of the data stream(s) to delete.
45
+ Can be a comma-separated list or wildcard pattern.
46
+ """
47
+ return self.search_client.delete_data_stream(name=name)
src/tools/document.py ADDED
@@ -0,0 +1,64 @@
1
+ from typing import Dict, Optional
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ class DocumentTools:
6
+ def __init__(self, search_client):
7
+ self.search_client = search_client
8
+
9
+ def register_tools(self, mcp: FastMCP):
10
+ @mcp.tool()
11
+ def search_documents(index: str, body: Dict) -> Dict:
12
+ """
13
+ Search for documents.
14
+
15
+ Args:
16
+ index: Name of the index
17
+ body: Search query
18
+ """
19
+ return self.search_client.search_documents(index=index, body=body)
20
+
21
+ @mcp.tool()
22
+ def index_document(index: str, document: Dict, id: Optional[str] = None) -> Dict:
23
+ """
24
+ Creates or updates a document in the index.
25
+
26
+ Args:
27
+ index: Name of the index
28
+ document: Document data
29
+ id: Optional document ID
30
+ """
31
+ return self.search_client.index_document(index=index, id=id, document=document)
32
+
33
+ @mcp.tool()
34
+ def get_document(index: str, id: str) -> Dict:
35
+ """
36
+ Get a document by ID.
37
+
38
+ Args:
39
+ index: Name of the index
40
+ id: Document ID
41
+ """
42
+ return self.search_client.get_document(index=index, id=id)
43
+
44
+ @mcp.tool()
45
+ def delete_document(index: str, id: str) -> Dict:
46
+ """
47
+ Delete a document by ID.
48
+
49
+ Args:
50
+ index: Name of the index
51
+ id: Document ID
52
+ """
53
+ return self.search_client.delete_document(index=index, id=id)
54
+
55
+ @mcp.tool()
56
+ def delete_by_query(index: str, body: Dict) -> Dict:
57
+ """
58
+ Deletes documents matching the provided query.
59
+
60
+ Args:
61
+ index: Name of the index
62
+ body: Query to match documents for deletion
63
+ """
64
+ return self.search_client.delete_by_query(index=index, body=body)
src/tools/general.py ADDED
@@ -0,0 +1,20 @@
1
+ from typing import Dict, Optional
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ class GeneralTools:
6
+ def __init__(self, search_client):
7
+ self.search_client = search_client
8
+ def register_tools(self, mcp: FastMCP):
9
+ @mcp.tool()
10
+ def general_api_request(method: str, path: str, params: Optional[Dict] = None, body: Optional[Dict] = None):
11
+ """Perform a general HTTP API request.
12
+ Use this tool for any Elasticsearch/OpenSearch API that does not have a dedicated tool.
13
+
14
+ Args:
15
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
16
+ path: API endpoint path
17
+ params: Query parameters
18
+ body: Request body
19
+ """
20
+ return self.search_client.general_api_request(method, path, params, body)
src/tools/index.py ADDED
@@ -0,0 +1,44 @@
1
+ from typing import Dict, Optional, List
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ class IndexTools:
6
+ def __init__(self, search_client):
7
+ self.search_client = search_client
8
+
9
+ def register_tools(self, mcp: FastMCP):
10
+ @mcp.tool()
11
+ def list_indices() -> List[Dict]:
12
+ """List all indices."""
13
+ return self.search_client.list_indices()
14
+
15
+ @mcp.tool()
16
+ def get_index(index: str) -> Dict:
17
+ """
18
+ Returns information (mappings, settings, aliases) about one or more indices.
19
+
20
+ Args:
21
+ index: Name of the index
22
+ """
23
+ return self.search_client.get_index(index=index)
24
+
25
+ @mcp.tool()
26
+ def create_index(index: str, body: Optional[Dict] = None) -> Dict:
27
+ """
28
+ Create a new index.
29
+
30
+ Args:
31
+ index: Name of the index
32
+ body: Optional index configuration including mappings and settings
33
+ """
34
+ return self.search_client.create_index(index=index, body=body)
35
+
36
+ @mcp.tool()
37
+ def delete_index(index: str) -> Dict:
38
+ """
39
+ Delete an index.
40
+
41
+ Args:
42
+ index: Name of the index
43
+ """
44
+ return self.search_client.delete_index(index=index)
src/tools/register.py ADDED
@@ -0,0 +1,89 @@
1
+ import logging
2
+ from typing import List, Type
3
+
4
+ from fastmcp import FastMCP
5
+
6
+ from src.clients import SearchClient
7
+ from src.clients.exceptions import with_exception_handling
8
+ from src.risk_config import risk_manager
9
+
10
+ class ToolsRegister:
11
+ """Class to handle registration of MCP tools."""
12
+
13
+ def __init__(self, logger: logging.Logger, search_client: SearchClient, mcp: FastMCP):
14
+ """
15
+ Initialize the tools register.
16
+
17
+ Args:
18
+ logger: Logger instance
19
+ search_client: Search client instance
20
+ mcp: FastMCP instance
21
+ """
22
+ self.logger = logger
23
+ self.search_client = search_client
24
+ self.mcp = mcp
25
+
26
+ def register_all_tools(self, tool_classes: List[Type]):
27
+ """
28
+ Register all tools with the MCP server.
29
+
30
+ Args:
31
+ tool_classes: List of tool classes to register
32
+ """
33
+ for tool_class in tool_classes:
34
+ self.logger.info(f"Registering tools from {tool_class.__name__}")
35
+ tool_instance = tool_class(self.search_client)
36
+
37
+ # Set logger and client attributes
38
+ tool_instance.logger = self.logger
39
+ tool_instance.search_client = self.search_client
40
+
41
+ # Check if risk management is enabled (high-risk operations are disabled)
42
+ if risk_manager.high_risk_ops_disabled:
43
+ # Add risk manager attributes for filtering
44
+ tool_instance.risk_manager = risk_manager
45
+ tool_instance.tool_class_name = tool_class.__name__
46
+ # Register tools with risk filtering
47
+ self._register_with_risk_filter(tool_instance)
48
+ else:
49
+ # Register tools with just exception handling (original way)
50
+ with_exception_handling(tool_instance, self.mcp)
51
+
52
+ def _register_with_risk_filter(self, tool_instance):
53
+ """
54
+ Register tools with risk filtering applied.
55
+ Only called when risk management is enabled.
56
+
57
+ Args:
58
+ tool_instance: The tool instance to register
59
+ """
60
+ # Save the original mcp.tool method
61
+ original_tool = self.mcp.tool
62
+
63
+ # Create a wrapper that filters based on risk
64
+ def risk_filter_wrapper(*args, **kwargs):
65
+ # Get the original decorator
66
+ decorator = original_tool(*args, **kwargs)
67
+
68
+ def risk_check_decorator(func):
69
+ operation_name = func.__name__
70
+
71
+ # Check if operation is allowed
72
+ if not risk_manager.is_operation_allowed(tool_instance.tool_class_name, operation_name):
73
+ # Don't register disabled tools - return a no-op function
74
+ def no_op(*args, **kwargs):
75
+ pass
76
+ return no_op
77
+
78
+ # If allowed, use the original decorator
79
+ return decorator(func)
80
+
81
+ return risk_check_decorator
82
+
83
+ try:
84
+ self.mcp.tool = risk_filter_wrapper
85
+ # This will wrap our risk_filter_wrapper with exception handling
86
+ with_exception_handling(tool_instance, self.mcp)
87
+ finally:
88
+ # Restore the original mcp.tool
89
+ self.mcp.tool = original_tool
src/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.17"