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.
- elasticsearch_mcp_server-2.0.17.dist-info/METADATA +555 -0
- elasticsearch_mcp_server-2.0.17.dist-info/RECORD +28 -0
- elasticsearch_mcp_server-2.0.17.dist-info/WHEEL +4 -0
- elasticsearch_mcp_server-2.0.17.dist-info/entry_points.txt +3 -0
- elasticsearch_mcp_server-2.0.17.dist-info/licenses/LICENSE +201 -0
- src/__init__.py +6 -0
- src/clients/__init__.py +44 -0
- src/clients/base.py +137 -0
- src/clients/common/__init__.py +6 -0
- src/clients/common/alias.py +20 -0
- src/clients/common/client.py +27 -0
- src/clients/common/cluster.py +12 -0
- src/clients/common/data_stream.py +18 -0
- src/clients/common/document.py +37 -0
- src/clients/common/general.py +10 -0
- src/clients/common/index.py +20 -0
- src/clients/exceptions.py +69 -0
- src/risk_config.py +77 -0
- src/server.py +166 -0
- src/tools/__init__.py +15 -0
- src/tools/alias.py +46 -0
- src/tools/cluster.py +17 -0
- src/tools/data_stream.py +47 -0
- src/tools/document.py +64 -0
- src/tools/general.py +20 -0
- src/tools/index.py +44 -0
- src/tools/register.py +89 -0
- src/version.py +1 -0
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()
|
src/tools/data_stream.py
ADDED
|
@@ -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"
|