elasticsearch-mcp-server 1.0.0__py3-none-any.whl → 2.0.0__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.

Potentially problematic release.


This version of elasticsearch-mcp-server might be problematic. Click here for more details.

@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elasticsearch-mcp-server
3
- Version: 1.0.0
4
- Summary: MCP Server for interacting with Elasticsearch
3
+ Version: 2.0.0
4
+ Summary: MCP Server for interacting with Elasticsearch and OpenSearch
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
7
7
  http://www.apache.org/licenses/
@@ -205,17 +205,23 @@ License: Apache License
205
205
  limitations under the License.
206
206
  License-File: LICENSE
207
207
  Requires-Python: >=3.10
208
+ Requires-Dist: anthropic>=0.49.0
208
209
  Requires-Dist: elasticsearch>=8.0.0
209
210
  Requires-Dist: fastmcp>=0.4.0
210
211
  Requires-Dist: mcp>=1.0.0
212
+ Requires-Dist: opensearch-py>=2.0.0
211
213
  Requires-Dist: python-dotenv>=1.0.0
214
+ Requires-Dist: tomli-w>=1.2.0
215
+ Requires-Dist: tomli>=2.2.1
212
216
  Description-Content-Type: text/markdown
213
217
 
214
- # Elasticsearch MCP Server
218
+ # Elasticsearch/OpenSearch MCP Server
219
+
220
+ [![smithery badge](https://smithery.ai/badge/elasticsearch-mcp-server)](https://smithery.ai/server/elasticsearch-mcp-server)
215
221
 
216
222
  ## Overview
217
223
 
218
- A Model Context Protocol (MCP) server implementation that provides Elasticsearch interaction. This server enables searching documents, analyzing indices, and managing cluster through a set of tools.
224
+ A Model Context Protocol (MCP) server implementation that provides Elasticsearch and OpenSearch interaction. This server enables searching documents, analyzing indices, and managing cluster through a set of tools.
219
225
 
220
226
  <a href="https://glama.ai/mcp/servers/b3po3delex"><img width="380" height="200" src="https://glama.ai/mcp/servers/b3po3delex/badge" alt="Elasticsearch MCP Server" /></a>
221
227
 
@@ -227,41 +233,67 @@ https://github.com/user-attachments/assets/f7409e31-fac4-4321-9c94-b0ff2ea7ff15
227
233
 
228
234
  ### Index Operations
229
235
 
230
- - `list_indices`: List all indices in the Elasticsearch cluster.
231
- - `get_mapping`: Retrieve the mapping configuration for a specific index.
232
- - `get_settings`: Get the settings configuration for a specific index.
236
+ - `list_indices`: List all indices.
237
+ - `get_index`: Returns information (mappings, settings, aliases) about one or more indices.
238
+ - `create_index`: Create a new index.
239
+ - `delete_index`: Delete an index.
233
240
 
234
241
  ### Document Operations
235
242
 
236
- - `search_documents`: Search documents in an index using Elasticsearch Query DSL.
243
+ - `search_documents`: Search for documents.
244
+ - `index_document`: Creates or updates a document in the index.
245
+ - `get_document`: Get a document by ID.
246
+ - `delete_document`: Delete a document by ID.
247
+ - `delete_by_query`: Deletes documents matching the provided query.
237
248
 
238
249
  ### Cluster Operations
239
250
 
240
- - `get_cluster_health`: Get health status of the cluster.
241
- - `get_cluster_stats`: Get statistical information about the cluster.
251
+ - `get_cluster_health`: Returns basic information about the health of the cluster.
252
+ - `get_cluster_stats`: Returns high-level overview of cluster statistics.
253
+
254
+ ### Alias Operations
255
+
256
+ - `list_aliases`: List all aliases.
257
+ - `get_alias`: Get alias information for a specific index.
258
+ - `put_alias`: Create or update an alias for a specific index.
259
+ - `delete_alias`: Delete an alias for a specific index.
242
260
 
261
+ ## Configure Environment Variables
243
262
 
244
- ## Start Elasticsearch Cluster
263
+ Copy the `.env.example` file to `.env` and update the values accordingly.
245
264
 
246
- Start the Elasticsearch cluster using Docker Compose:
265
+ ## Start Elasticsearch/OpenSearch Cluster
266
+
267
+ Start the Elasticsearch/OpenSearch cluster using Docker Compose:
247
268
 
248
269
  ```bash
249
- docker-compose up -d
270
+ # For Elasticsearch
271
+ docker-compose -f docker-compose-elasticsearch.yml up -d
272
+
273
+ # For OpenSearch
274
+ docker-compose -f docker-compose-opensearch.yml up -d
250
275
  ```
251
276
 
252
- This will start a 3-node Elasticsearch cluster and Kibana. Default Elasticsearch username `elastic`, password `test123`.
277
+ The default Elasticsearch username is `elastic` and password is `test123`. The default OpenSearch username is `admin` and password is `admin`.
253
278
 
254
- You can access Kibana from http://localhost:5601.
279
+ You can access Kibana/OpenSearch Dashboards from http://localhost:5601.
255
280
 
256
281
  ## Usage with Claude Desktop
257
282
 
258
- Add the following configuration to Claude Desktop's config file `claude_desktop_config.json`.
283
+ ### Option 1: Installing via Smithery
259
284
 
260
- ### Option 1: Using uvx (Recommended)
285
+ To install Elasticsearch Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/elasticsearch-mcp-server):
261
286
 
262
- Using `uvx` will automatically install the package from PyPI, no need to clone the repository locally
287
+ ```bash
288
+ npx -y @smithery/cli install elasticsearch-mcp-server --client claude
289
+ ```
290
+
291
+ ### Option 2: Using uvx
292
+
293
+ Using `uvx` will automatically install the package from PyPI, no need to clone the repository locally. Add the following configuration to Claude Desktop's config file `claude_desktop_config.json`.
263
294
 
264
295
  ```json
296
+ // For Elasticsearch
265
297
  {
266
298
  "mcpServers": {
267
299
  "elasticsearch-mcp-server": {
@@ -270,23 +302,41 @@ Using `uvx` will automatically install the package from PyPI, no need to clone t
270
302
  "elasticsearch-mcp-server"
271
303
  ],
272
304
  "env": {
273
- "ELASTIC_HOST": "https://localhost:9200",
274
- "ELASTIC_USERNAME": "elastic",
275
- "ELASTIC_PASSWORD": "test123"
305
+ "ELASTICSEARCH_HOST": "https://localhost:9200",
306
+ "ELASTICSEARCH_USERNAME": "elastic",
307
+ "ELASTICSEARCH_PASSWORD": "test123"
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ // For OpenSearch
314
+ {
315
+ "mcpServers": {
316
+ "opensearch-mcp-server": {
317
+ "command": "uvx",
318
+ "args": [
319
+ "opensearch-mcp-server"
320
+ ],
321
+ "env": {
322
+ "OPENSEARCH_HOST": "https://localhost:9200",
323
+ "OPENSEARCH_USERNAME": "admin",
324
+ "OPENSEARCH_PASSWORD": "admin"
276
325
  }
277
326
  }
278
327
  }
279
328
  }
280
329
  ```
281
330
 
282
- ### Option 2: Using uv with local development
331
+ ### Option 3: Using uv with local development
283
332
 
284
- Using `uv` requires cloning the repository locally and specifying the path to the source code.
333
+ Using `uv` requires cloning the repository locally and specifying the path to the source code. Add the following configuration to Claude Desktop's config file `claude_desktop_config.json`.
285
334
 
286
335
  ```json
336
+ // For Elasticsearch
287
337
  {
288
338
  "mcpServers": {
289
- "elasticsearch": {
339
+ "elasticsearch-mcp-server": {
290
340
  "command": "uv",
291
341
  "args": [
292
342
  "--directory",
@@ -295,9 +345,29 @@ Using `uv` requires cloning the repository locally and specifying the path to th
295
345
  "elasticsearch-mcp-server"
296
346
  ],
297
347
  "env": {
298
- "ELASTIC_HOST": "https://localhost:9200",
299
- "ELASTIC_USERNAME": "elastic",
300
- "ELASTIC_PASSWORD": "test123"
348
+ "ELASTICSEARCH_HOST": "https://localhost:9200",
349
+ "ELASTICSEARCH_USERNAME": "elastic",
350
+ "ELASTICSEARCH_PASSWORD": "test123"
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ // For OpenSearch
357
+ {
358
+ "mcpServers": {
359
+ "opensearch-mcp-server": {
360
+ "command": "uv",
361
+ "args": [
362
+ "--directory",
363
+ "path/to/src/elasticsearch_mcp_server",
364
+ "run",
365
+ "opensearch-mcp-server"
366
+ ],
367
+ "env": {
368
+ "OPENSEARCH_HOST": "https://localhost:9200",
369
+ "OPENSEARCH_USERNAME": "admin",
370
+ "OPENSEARCH_PASSWORD": "admin"
301
371
  }
302
372
  }
303
373
  }
@@ -309,11 +379,17 @@ Using `uv` requires cloning the repository locally and specifying the path to th
309
379
 
310
380
  Restart Claude Desktop to load the new MCP server.
311
381
 
312
- Now you can interact with your Elasticsearch cluster through Claude using natural language commands like:
382
+ Now you can interact with your Elasticsearch/OpenSearch cluster through Claude using natural language commands like:
313
383
  - "List all indices in the cluster"
314
384
  - "How old is the student Bob?"
315
385
  - "Show me the cluster health status"
316
386
 
387
+ ## Usage with Anthropic MCP Client
388
+
389
+ ```python
390
+ uv run mcp_client/client.py src/server.py
391
+ ```
392
+
317
393
  ## License
318
394
 
319
- This project is licensed under the Apache License Version 2.0 - see the [LICENSE](LICENSE) file for details.
395
+ This project is licensed under the Apache License Version 2.0 - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,22 @@
1
+ src/__init__.py,sha256=aNKcThftSLh9IjOTA-UUpoRzIm4R0WwXKGAzykwecmU,211
2
+ src/server.py,sha256=BtNr3oKiRK1rbnocFIKzwpukY87_AylzKh_ffwP6Zr8,2153
3
+ src/clients/__init__.py,sha256=3UezAt9422S-7BvMiCo2Y9pmATVutorwsIQXP_g_CkA,1221
4
+ src/clients/base.py,sha256=vTe4I62ruO2bEeSAag-2B5fJOWJDXllKbo8qA0h4VkM,2160
5
+ src/clients/exceptions.py,sha256=NYF3KVw-9aAgCViin5OuBI6miMEPS5QsdHx6bcis1wc,2493
6
+ src/clients/common/__init__.py,sha256=VgvgxFpESn2wAuJmH0XM_Ej2izI7dxK7QJe9wG4fmW0,211
7
+ src/clients/common/alias.py,sha256=rB53TSld5x2vZyDNAwyEdnh1KWUXMSD7h5fSv_ubR2Q,759
8
+ src/clients/common/client.py,sha256=GD-V97Dj8c2XGUYt58vqCggkk-k8WJePPgngBDWO5aM,973
9
+ src/clients/common/cluster.py,sha256=pd5BVpqqDU6Lck3K704eEdhgFgzt9NstotWQLyG9zFM,401
10
+ src/clients/common/document.py,sha256=ZzZiXDf_UhlN2FCmqW3drVjIZ07kGYP9yth_sgsJGPc,1623
11
+ src/clients/common/index.py,sha256=vyH5iXlJe5JLcDK6fhCCEPN-tgGm6zP5ilwGPdWCXbY,776
12
+ src/tools/__init__.py,sha256=qiMRNUKlpNGnqbrkgBHGa7El1Pw_0a2qUJnuk_Q8LtY,324
13
+ src/tools/alias.py,sha256=p9TD4gXkGRGWHTYfvCg7G2hj-Uch9jwDXJNUY1hSD0Y,1376
14
+ src/tools/cluster.py,sha256=XRAG-uxdfrieYX1ov_cBb66IYXaa8OoSWCUfXvNauy0,587
15
+ src/tools/document.py,sha256=XZTVuk4di9VEwWMZN7jyDVnzoOiXkb4FBrXF44sVXTs,2052
16
+ src/tools/index.py,sha256=7KNPtElTFelkjRSvdMqPBx9nx_9Uk01OnTMeVo7YeCs,1345
17
+ src/tools/register.py,sha256=wrG2P6-YPW77bTg1j_ELp8omWRYsJjFeOHUy_unHe6Y,1344
18
+ elasticsearch_mcp_server-2.0.0.dist-info/METADATA,sha256=ro27Cz5Px51wVUv8lz-ASLObuMlWXQlbcw-RWT6SonI,18512
19
+ elasticsearch_mcp_server-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ elasticsearch_mcp_server-2.0.0.dist-info/entry_points.txt,sha256=ImfJnUwMpQUBmu-1MeBG_P0dwamfXKQh82mBKW7tWNY,138
21
+ elasticsearch_mcp_server-2.0.0.dist-info/licenses/LICENSE,sha256=DBsjuP5FR51d9kaUdXlVBuBv3cQ_I_adq9gefYQ9FcY,11339
22
+ elasticsearch_mcp_server-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ elasticsearch-mcp-server = src.server:elasticsearch_mcp_server
3
+ opensearch-mcp-server = src.server:opensearch_mcp_server
src/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ Search MCP Server package.
3
+ """
4
+ from src.server import elasticsearch_mcp_server, opensearch_mcp_server, run_search_server
5
+
6
+ __all__ = ['elasticsearch_mcp_server', 'opensearch_mcp_server', 'run_search_server']
@@ -0,0 +1,42 @@
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ from src.clients.common.client import SearchClient
6
+ from src.clients.exceptions import handle_search_exceptions
7
+
8
+ def create_search_client(engine_type: str) -> SearchClient:
9
+ """
10
+ Create a search client for the specified engine type.
11
+
12
+ Args:
13
+ engine_type: Type of search engine to use ("elasticsearch" or "opensearch")
14
+
15
+ Returns:
16
+ A search client instance
17
+ """
18
+ # Load configuration from environment variables
19
+ load_dotenv()
20
+
21
+ # Get configuration from environment variables
22
+ prefix = engine_type.upper()
23
+ hosts_str = os.environ.get(f"{prefix}_HOSTS", "https://localhost:9200")
24
+ hosts = [host.strip() for host in hosts_str.split(",")]
25
+ username = os.environ.get(f"{prefix}_USERNAME")
26
+ password = os.environ.get(f"{prefix}_PASSWORD")
27
+ verify_certs = os.environ.get(f"{prefix}_VERIFY_CERTS", "false").lower() == "true"
28
+
29
+ config = {
30
+ "hosts": hosts,
31
+ "username": username,
32
+ "password": password,
33
+ "verify_certs": verify_certs
34
+ }
35
+
36
+ return SearchClient(config, engine_type)
37
+
38
+ __all__ = [
39
+ 'create_search_client',
40
+ 'handle_search_exceptions',
41
+ 'SearchClient',
42
+ ]
src/clients/base.py ADDED
@@ -0,0 +1,54 @@
1
+ from abc import ABC
2
+ import logging
3
+ import warnings
4
+ from typing import Dict
5
+
6
+ class SearchClientBase(ABC):
7
+ def __init__(self, config: Dict, engine_type: str):
8
+ """
9
+ Initialize the search client.
10
+
11
+ Args:
12
+ config: Configuration dictionary with connection parameters
13
+ engine_type: Type of search engine to use ("elasticsearch" or "opensearch")
14
+ """
15
+ self.logger = logging.getLogger()
16
+ self.config = config
17
+ self.engine_type = engine_type
18
+
19
+ # Extract common configuration
20
+ hosts = config.get("hosts")
21
+ username = config.get("username")
22
+ password = config.get("password")
23
+ verify_certs = config.get("verify_certs", False)
24
+
25
+ # Disable insecure request warnings if verify_certs is False
26
+ if not verify_certs:
27
+ warnings.filterwarnings("ignore", message=".*verify_certs=False is insecure.*")
28
+ warnings.filterwarnings("ignore", message=".*Unverified HTTPS request is being made to host.*")
29
+
30
+ try:
31
+ import urllib3
32
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
33
+ except ImportError:
34
+ pass
35
+
36
+ # Initialize client based on engine type
37
+ if engine_type == "elasticsearch":
38
+ from elasticsearch import Elasticsearch
39
+ self.client = Elasticsearch(
40
+ hosts=hosts,
41
+ basic_auth=(username, password) if username and password else None,
42
+ verify_certs=verify_certs
43
+ )
44
+ self.logger.info(f"Elasticsearch client initialized with hosts: {hosts}")
45
+ elif engine_type == "opensearch":
46
+ from opensearchpy import OpenSearch
47
+ self.client = OpenSearch(
48
+ hosts=hosts,
49
+ http_auth=(username, password) if username and password else None,
50
+ verify_certs=verify_certs
51
+ )
52
+ self.logger.info(f"OpenSearch client initialized with hosts: {hosts}")
53
+ else:
54
+ raise ValueError(f"Unsupported engine type: {engine_type}")
@@ -0,0 +1,6 @@
1
+ from .index import IndexClient
2
+ from .document import DocumentClient
3
+ from .cluster import ClusterClient
4
+ from .alias import AliasClient
5
+
6
+ __all__ = ['IndexClient', 'DocumentClient', 'ClusterClient', 'AliasClient']
@@ -0,0 +1,20 @@
1
+ from typing import Dict
2
+
3
+ from src.clients.base import SearchClientBase
4
+
5
+ class AliasClient(SearchClientBase):
6
+ def list_aliases(self) -> Dict:
7
+ """Get all aliases."""
8
+ return self.client.cat.aliases()
9
+
10
+ def get_alias(self, index: str) -> Dict:
11
+ """Get aliases for the specified index."""
12
+ return self.client.indices.get_alias(index=index)
13
+
14
+ def put_alias(self, index: str, name: str, body: Dict) -> Dict:
15
+ """Creates or updates an alias."""
16
+ return self.client.indices.put_alias(index=index, name=name, body=body)
17
+
18
+ def delete_alias(self, index: str, name: str) -> Dict:
19
+ """Delete an alias for the specified index."""
20
+ return self.client.indices.delete_alias(index=index, name=name)
@@ -0,0 +1,25 @@
1
+ from typing import Dict
2
+
3
+ from src.clients.common.alias import AliasClient
4
+ from src.clients.common.cluster import ClusterClient
5
+ from src.clients.common.document import DocumentClient
6
+ from src.clients.common.index import IndexClient
7
+
8
+ class SearchClient(IndexClient, DocumentClient, ClusterClient, AliasClient):
9
+ """
10
+ Unified search client that combines all search functionality.
11
+
12
+ This class uses multiple inheritance to combine all specialized client implementations
13
+ (index, document, cluster, alias) into a single unified client.
14
+ """
15
+
16
+ def __init__(self, config: Dict, engine_type: str):
17
+ """
18
+ Initialize the search client.
19
+
20
+ Args:
21
+ config: Configuration dictionary with connection parameters
22
+ engine_type: Type of search engine to use ("elasticsearch" or "opensearch")
23
+ """
24
+ super().__init__(config, engine_type)
25
+ self.logger.info(f"Initialized the {engine_type} client")
@@ -0,0 +1,12 @@
1
+ from typing import Dict
2
+
3
+ from src.clients.base import SearchClientBase
4
+
5
+ class ClusterClient(SearchClientBase):
6
+ def get_cluster_health(self) -> Dict:
7
+ """Get cluster health information from OpenSearch."""
8
+ return self.client.cluster.health()
9
+
10
+ def get_cluster_stats(self) -> Dict:
11
+ """Get cluster statistics from OpenSearch."""
12
+ return self.client.cluster.stats()
@@ -0,0 +1,37 @@
1
+ from typing import Dict, Optional
2
+
3
+ from src.clients.base import SearchClientBase
4
+
5
+ class DocumentClient(SearchClientBase):
6
+ def search_documents(self, index: str, body: Dict) -> Dict:
7
+ """Search for documents in the index."""
8
+ return self.client.search(index=index, body=body)
9
+
10
+ def index_document(self, index: str, document: Dict, id: Optional[str] = None) -> Dict:
11
+ """Creates a new document in the index."""
12
+ # Handle parameter name differences between Elasticsearch and OpenSearch
13
+ if self.engine_type == "elasticsearch":
14
+ # For Elasticsearch: index(index, document, id=None, ...)
15
+ if id is not None:
16
+ return self.client.index(index=index, document=document, id=id)
17
+ else:
18
+ return self.client.index(index=index, document=document)
19
+ else:
20
+ # For OpenSearch: index(index, body, id=None, ...)
21
+ if id is not None:
22
+ return self.client.index(index=index, body=document, id=id)
23
+ else:
24
+ return self.client.index(index=index, body=document)
25
+
26
+ def get_document(self, index: str, id: str) -> Dict:
27
+ """Get a document by ID."""
28
+ return self.client.get(index=index, id=id)
29
+
30
+ def delete_document(self, index: str, id: str) -> Dict:
31
+ """Removes a document from the index."""
32
+ return self.client.delete(index=index, id=id)
33
+
34
+ def delete_by_query(self, index: str, body: Dict) -> Dict:
35
+ """Deletes documents matching the provided query."""
36
+ return self.client.delete_by_query(index=index, body=body)
37
+
@@ -0,0 +1,20 @@
1
+ from typing import Dict, Optional
2
+
3
+ from src.clients.base import SearchClientBase
4
+
5
+ class IndexClient(SearchClientBase):
6
+ def list_indices(self) -> Dict:
7
+ """List all indices."""
8
+ return self.client.cat.indices()
9
+
10
+ def get_index(self, index: str) -> Dict:
11
+ """Returns information (mappings, settings, aliases) about one or more indices."""
12
+ return self.client.indices.get(index=index)
13
+
14
+ def create_index(self, index: str, body: Optional[Dict] = None) -> Dict:
15
+ """Creates an index with optional settings and mappings."""
16
+ return self.client.indices.create(index=index, body=body)
17
+
18
+ def delete_index(self, index: str) -> Dict:
19
+ """Delete an index."""
20
+ return self.client.indices.delete(index=index)
@@ -0,0 +1,69 @@
1
+ import functools
2
+ import logging
3
+ from typing import TypeVar, Callable
4
+
5
+ from fastmcp import FastMCP
6
+ from mcp.types import TextContent
7
+
8
+ T = TypeVar('T')
9
+
10
+ def handle_search_exceptions(func: Callable[..., T]) -> Callable[..., list[TextContent]]:
11
+ """
12
+ Decorator to handle exceptions in search client operations.
13
+
14
+ Args:
15
+ func: The function to decorate
16
+
17
+ Returns:
18
+ Decorated function that handles exceptions
19
+ """
20
+ @functools.wraps(func)
21
+ def wrapper(*args, **kwargs):
22
+ logger = logging.getLogger()
23
+ try:
24
+ return func(*args, **kwargs)
25
+ except Exception as e:
26
+ logger.error(f"Unexpected error in {func.__name__}: {e}")
27
+ return [TextContent(type="text", text=f"Unexpected error in {func.__name__}: {str(e)}")]
28
+
29
+ return wrapper
30
+
31
+ def with_exception_handling(tool_instance: object, mcp: FastMCP) -> None:
32
+ """
33
+ Register tools from a tool instance with automatic exception handling applied to all tools.
34
+
35
+ This function temporarily replaces mcp.tool with a wrapped version that automatically
36
+ applies the handle_search_exceptions decorator to all registered tool methods.
37
+
38
+ Args:
39
+ tool_instance: The tool instance that has a register_tools method
40
+ mcp: The FastMCP instance used for tool registration
41
+ """
42
+ # Save the original tool method
43
+ original_tool = mcp.tool
44
+
45
+ @functools.wraps(original_tool)
46
+ def wrapped_tool(*args, **kwargs):
47
+ # Get the original decorator
48
+ decorator = original_tool(*args, **kwargs)
49
+
50
+ # Return a new decorator that applies both the exception handler and original decorator
51
+ def combined_decorator(func):
52
+ # First apply the exception handling decorator
53
+ wrapped_func = handle_search_exceptions(func)
54
+ # Then apply the original mcp.tool decorator
55
+ return decorator(wrapped_func)
56
+
57
+ return combined_decorator
58
+
59
+ try:
60
+ # Temporarily replace mcp.tool with our wrapped version
61
+ mcp.tool = wrapped_tool
62
+
63
+ # Call the registration method on the tool instance
64
+ tool_instance.register_tools(mcp)
65
+ finally:
66
+ # Restore the original mcp.tool to avoid affecting other code that might use mcp.tool
67
+ # This ensures that our modification is isolated to just this tool registration
68
+ # and prevents multiple nested decorators if register_all_tools is called multiple times
69
+ mcp.tool = original_tool
src/server.py ADDED
@@ -0,0 +1,71 @@
1
+ import logging
2
+ import sys
3
+
4
+ from fastmcp import FastMCP
5
+
6
+ from src.clients import create_search_client
7
+ from src.tools.alias import AliasTools
8
+ from src.tools.cluster import ClusterTools
9
+ from src.tools.document import DocumentTools
10
+ from src.tools.index import IndexTools
11
+ from src.tools.register import ToolsRegister
12
+
13
+ class SearchMCPServer:
14
+ def __init__(self, engine_type):
15
+ # Set engine type
16
+ self.engine_type = engine_type
17
+ self.name = f"{self.engine_type}_mcp_server"
18
+ self.mcp = FastMCP(self.name)
19
+ self.logger = logging.getLogger()
20
+ self.logger.info(f"Initializing {self.name}...")
21
+
22
+ # Create the corresponding search client
23
+ self.search_client = create_search_client(self.engine_type)
24
+
25
+ # Initialize tools
26
+ self._register_tools()
27
+
28
+ def _register_tools(self):
29
+ """Register all MCP tools."""
30
+ # Create a tools register
31
+ register = ToolsRegister(self.logger, self.search_client, self.mcp)
32
+
33
+ # Define all tool classes to register
34
+ tool_classes = [
35
+ IndexTools,
36
+ DocumentTools,
37
+ ClusterTools,
38
+ AliasTools
39
+ ]
40
+ # Register all tools
41
+ register.register_all_tools(tool_classes)
42
+
43
+ def run(self):
44
+ """Run the MCP server."""
45
+ self.mcp.run()
46
+
47
+ def run_search_server(engine_type):
48
+ """Run search server with specified engine type."""
49
+ server = SearchMCPServer(engine_type=engine_type)
50
+ server.run()
51
+
52
+ def elasticsearch_mcp_server():
53
+ """Entry point for Elasticsearch MCP server."""
54
+ run_search_server(engine_type="elasticsearch")
55
+
56
+ def opensearch_mcp_server():
57
+ """Entry point for OpenSearch MCP server."""
58
+ run_search_server(engine_type="opensearch")
59
+
60
+ if __name__ == "__main__":
61
+ # Default to Elasticsearch
62
+ engine_type = "elasticsearch"
63
+
64
+ # If command line arguments are provided, use the first argument as the engine type
65
+ if len(sys.argv) > 1:
66
+ engine_type = sys.argv[1].lower()
67
+
68
+ if engine_type == "opensearch":
69
+ opensearch_mcp_server()
70
+ else:
71
+ elasticsearch_mcp_server()
src/tools/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from src.tools.index import IndexTools
2
+ from src.tools.document import DocumentTools
3
+ from src.tools.cluster import ClusterTools
4
+ from src.tools.alias import AliasTools
5
+ from src.tools.register import ToolsRegister
6
+
7
+ __all__ = [
8
+ 'IndexTools',
9
+ 'DocumentTools',
10
+ 'ClusterTools',
11
+ 'AliasTools',
12
+ 'ToolsRegister',
13
+ ]
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/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/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,41 @@
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
+
9
+ class ToolsRegister:
10
+ """Class to handle registration of MCP tools."""
11
+
12
+ def __init__(self, logger: logging.Logger, search_client: SearchClient, mcp: FastMCP):
13
+ """
14
+ Initialize the tools register.
15
+
16
+ Args:
17
+ logger: Logger instance
18
+ search_client: Search client instance
19
+ mcp: FastMCP instance
20
+ """
21
+ self.logger = logger
22
+ self.search_client = search_client
23
+ self.mcp = mcp
24
+
25
+ def register_all_tools(self, tool_classes: List[Type]):
26
+ """
27
+ Register all tools with the MCP server.
28
+
29
+ Args:
30
+ tool_classes: List of tool classes to register
31
+ """
32
+ for tool_class in tool_classes:
33
+ self.logger.info(f"Registering tools from {tool_class.__name__}")
34
+ tool_instance = tool_class(self.search_client)
35
+
36
+ # Set logger and client attributes
37
+ tool_instance.logger = self.logger
38
+ tool_instance.search_client = self.search_client
39
+
40
+ # Register tools with automatic exception handling
41
+ with_exception_handling(tool_instance, self.mcp)
@@ -1,10 +0,0 @@
1
- from . import server
2
-
3
-
4
- def main():
5
- """Main entry point for the package."""
6
- server.main()
7
-
8
-
9
- # Optionally expose other important items at package level
10
- __all__ = ["main", "server"]
@@ -1,40 +0,0 @@
1
- import logging
2
- import os
3
- from dotenv import load_dotenv
4
- from elasticsearch import Elasticsearch
5
- import warnings
6
-
7
- class ElasticsearchClient:
8
- def __init__(self, logger: logging.Logger):
9
- self.logger = logger
10
- self.es_client = self._create_elasticsearch_client()
11
-
12
- def _get_es_config(self):
13
- """Get Elasticsearch configuration from environment variables."""
14
- # Load environment variables from .env file
15
- load_dotenv()
16
- config = {
17
- "host": os.getenv("ELASTIC_HOST"),
18
- "username": os.getenv("ELASTIC_USERNAME"),
19
- "password": os.getenv("ELASTIC_PASSWORD")
20
- }
21
-
22
- if not all([config["username"], config["password"]]):
23
- self.logger.error("Missing required Elasticsearch configuration. Please check environment variables:")
24
- self.logger.error("ELASTIC_USERNAME and ELASTIC_PASSWORD are required")
25
- raise ValueError("Missing required Elasticsearch configuration")
26
-
27
- return config
28
-
29
- def _create_elasticsearch_client(self) -> Elasticsearch:
30
- """Create and return an Elasticsearch client using configuration from environment."""
31
- config = self._get_es_config()
32
-
33
- # Disable SSL warnings
34
- warnings.filterwarnings("ignore", message=".*TLS with verify_certs=False is insecure.*",)
35
-
36
- return Elasticsearch(
37
- config["host"],
38
- basic_auth=(config["username"], config["password"]),
39
- verify_certs=False
40
- )
@@ -1,41 +0,0 @@
1
- #!/usr/bin/env python3
2
- import logging
3
- from fastmcp import FastMCP
4
- from .tools.index import IndexTools
5
- from .tools.document import DocumentTools
6
- from .tools.cluster import ClusterTools
7
-
8
- class ElasticsearchMCPServer:
9
- def __init__(self):
10
- self.name = "elasticsearch_mcp_server"
11
- self.mcp = FastMCP(self.name)
12
-
13
- # Configure logging
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
17
- )
18
- self.logger = logging.getLogger(self.name)
19
-
20
- # Initialize tools
21
- self._register_tools()
22
-
23
- def _register_tools(self):
24
- """Register all MCP tools."""
25
- # Initialize tool classes
26
- index_tools = IndexTools(self.logger)
27
- document_tools = DocumentTools(self.logger)
28
- cluster_tools = ClusterTools(self.logger)
29
-
30
- # Register tools from each module
31
- index_tools.register_tools(self.mcp)
32
- document_tools.register_tools(self.mcp)
33
- cluster_tools.register_tools(self.mcp)
34
-
35
- def run(self):
36
- """Run the MCP server."""
37
- self.mcp.run()
38
-
39
- def main():
40
- server = ElasticsearchMCPServer()
41
- server.run()
@@ -1,38 +0,0 @@
1
- import logging
2
- from typing import Dict, Any
3
- from ..es_client import ElasticsearchClient
4
- from mcp.types import TextContent
5
-
6
- class ClusterTools(ElasticsearchClient):
7
- def register_tools(self, mcp: Any):
8
- """Register cluster-related tools."""
9
-
10
- @mcp.tool(description="Get cluster health status")
11
- async def get_cluster_health() -> list[TextContent]:
12
- """
13
- Get health status of the Elasticsearch cluster.
14
- Returns information about the number of nodes, shards, etc.
15
- """
16
- self.logger.info("Getting cluster health")
17
- try:
18
- response = self.es_client.cluster.health()
19
- return [TextContent(type="text", text=str(response))]
20
- except Exception as e:
21
- self.logger.error(f"Error getting cluster health: {e}")
22
- return [TextContent(type="text", text=f"Error: {str(e)}")]
23
-
24
- @mcp.tool(description="Get cluster statistics")
25
- async def get_cluster_stats() -> list[TextContent]:
26
- """
27
- Get statistics from a cluster wide perspective.
28
- The API returns basic index metrics (shard numbers, store size, memory usage) and information
29
- about the current nodes that form the cluster (number, roles, os, jvm versions, memory usage, cpu and installed plugins).
30
- https://www.elastic.co/guide/en/elasticsearch/reference/8.17/cluster-stats.html
31
- """
32
- self.logger.info("Getting cluster stats")
33
- try:
34
- response = self.es_client.cluster.stats()
35
- return [TextContent(type="text", text=str(response))]
36
- except Exception as e:
37
- self.logger.error(f"Error getting cluster stats: {e}")
38
- return [TextContent(type="text", text=f"Error: {str(e)}")]
@@ -1,25 +0,0 @@
1
- import logging
2
- from typing import Dict, Any
3
- from ..es_client import ElasticsearchClient
4
- from mcp.types import TextContent
5
-
6
- class DocumentTools(ElasticsearchClient):
7
- def register_tools(self, mcp: Any):
8
- """Register document-related tools."""
9
-
10
- @mcp.tool(description="Search documents in an index with a custom query")
11
- async def search_documents(index: str, body: dict) -> list[TextContent]:
12
- """
13
- Search documents in a specified index using a custom query.
14
-
15
- Args:
16
- index: Name of the index to search
17
- body: Elasticsearch query DSL
18
- """
19
- self.logger.info(f"Searching in index: {index} with query: {body}")
20
- try:
21
- response = self.es_client.search(index=index, body=body)
22
- return [TextContent(type="text", text=str(response))]
23
- except Exception as e:
24
- self.logger.error(f"Error searching documents: {e}")
25
- return [TextContent(type="text", text=f"Error: {str(e)}")]
@@ -1,51 +0,0 @@
1
- import logging
2
- from typing import Dict, Any
3
- from ..es_client import ElasticsearchClient
4
- from mcp.types import TextContent
5
-
6
- class IndexTools(ElasticsearchClient):
7
- def register_tools(self, mcp: Any):
8
- """Register index-related tools."""
9
-
10
- @mcp.tool(description="List all indices in the Elasticsearch cluster")
11
- async def list_indices() -> list[TextContent]:
12
- """List all indices in the Elasticsearch cluster."""
13
- self.logger.info("Listing indices...")
14
- try:
15
- indices = self.es_client.cat.indices(format="json")
16
- return [TextContent(type="text", text=str(indices))]
17
- except Exception as e:
18
- self.logger.error(f"Error listing indices: {e}")
19
- return [TextContent(type="text", text=f"Error: {str(e)}")]
20
-
21
- @mcp.tool(description="Get index mapping")
22
- async def get_mapping(index: str) -> list[TextContent]:
23
- """
24
- Get the mapping for an index.
25
-
26
- Args:
27
- index: Name of the index
28
- """
29
- self.logger.info(f"Getting mapping for index: {index}")
30
- try:
31
- response = self.es_client.indices.get_mapping(index=index)
32
- return [TextContent(type="text", text=str(response))]
33
- except Exception as e:
34
- self.logger.error(f"Error getting mapping: {e}")
35
- return [TextContent(type="text", text=f"Error: {str(e)}")]
36
-
37
- @mcp.tool(description="Get index settings")
38
- async def get_settings(index: str) -> list[TextContent]:
39
- """
40
- Get the settings for an index.
41
-
42
- Args:
43
- index: Name of the index
44
- """
45
- self.logger.info(f"Getting settings for index: {index}")
46
- try:
47
- response = self.es_client.indices.get_settings(index=index)
48
- return [TextContent(type="text", text=str(response))]
49
- except Exception as e:
50
- self.logger.error(f"Error getting settings: {e}")
51
- return [TextContent(type="text", text=f"Error: {str(e)}")]
@@ -1,11 +0,0 @@
1
- elasticsearch_mcp_server/__init__.py,sha256=wGsW3VlozSA6CWKQSuUTaLZb31m4dNN8ACNG6xRwOyU,186
2
- elasticsearch_mcp_server/es_client.py,sha256=n7RYcB97DgCIkrY1ryP4Ubq3IJxfnKqzvXMXRCbOXr8,1510
3
- elasticsearch_mcp_server/server.py,sha256=RGKJKk6EpnM1etM8CYXoR_A4OjHqlXcIyRqgJfNJksg,1212
4
- elasticsearch_mcp_server/tools/cluster.py,sha256=bGShKKDCgHW0GZoPd8ty__yX1Yn_dTwOH4hGGQsO5l8,1849
5
- elasticsearch_mcp_server/tools/document.py,sha256=PDD63Etp1MS0G8N3IDg1j-hG41r5OFe4adlotnyuZHU,1075
6
- elasticsearch_mcp_server/tools/index.py,sha256=Zgc90bVNH18LaHs4CXIRDAdcAdJyxx_RN788xZjLA-A,2174
7
- elasticsearch_mcp_server-1.0.0.dist-info/METADATA,sha256=2aJ3yVM0e-zCowyEEOQYmZw78ii3qwLLdJ5RRzAv7qQ,16225
8
- elasticsearch_mcp_server-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- elasticsearch_mcp_server-1.0.0.dist-info/entry_points.txt,sha256=sAW_y-4MkO9T00GbIb_tLQikrTPF94uHU1YkbWMj65g,75
10
- elasticsearch_mcp_server-1.0.0.dist-info/licenses/LICENSE,sha256=DBsjuP5FR51d9kaUdXlVBuBv3cQ_I_adq9gefYQ9FcY,11339
11
- elasticsearch_mcp_server-1.0.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- elasticsearch-mcp-server = elasticsearch_mcp_server:main