#
tokens: 8738/50000 14/14 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .DS_Store
├── docker-compose.yml
├── Dockerfile
├── images
│   ├── .DS_Store
│   ├── claude1.png
│   ├── mcpDev0.png
│   ├── mcpDev1.png
│   └── osclientTest0.png
├── LICENSE
├── pyproject.toml
├── README.md
├── smithery.yaml
├── src
│   ├── .DS_Store
│   └── mcp-server-opensearch
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── AsyncOpenSearchClient.cpython-310.pyc
│       │   ├── demo.cpython-310.pyc
│       │   ├── demo.cpython-312.pyc
│       │   ├── opensearch.cpython-310.pyc
│       │   ├── OpenSearchClient.cpython-310.pyc
│       │   ├── OpenSearchClient.cpython-312.pyc
│       │   ├── OpenSearchClient.cpython-313.pyc
│       │   ├── server.cpython-310.pyc
│       │   ├── server.cpython-313.pyc
│       │   └── test_opensearch.cpython-310.pyc
│       ├── AsyncOpenSearchClient.py
│       ├── demo.py
│       ├── OpenSearchClient.py
│       ├── server.py
│       ├── serverTest.py
│       ├── test_AsyncClient.py
│       └── test_opensearch.py
├── test_OpenSearchClient.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # mcp-server-opensearch: An OpenSearch MCP Server
  2 | [![smithery badge](https://smithery.ai/badge/@ibrooksSDX/mcp-server-opensearch)](https://smithery.ai/server/@ibrooksSDX/mcp-server-opensearch)
  3 | 
  4 | > The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.
  5 | 
  6 | This repository is an example of how to create a MCP server for [OpenSearch](https://opensearch.org/), a distributed search and analytics engine.
  7 | 
  8 | # Under Contruction 
  9 | 
 10 | ![image1](./images/claude1.png)
 11 | ![image2](./images/mcpDev1.png)
 12 | 
 13 | 
 14 | ## Current Blocker - Async Client from OpenSearch isn't installing
 15 | 
 16 | [Open Search Async Client Docs](https://github.com/opensearch-project/opensearch-py/blob/main/guides/async.m) 
 17 | 
 18 | ```shell
 19 | pip install opensearch-py[async]
 20 | zsh: no matches found: opensearch-py[async]
 21 | ```
 22 | 
 23 | ## Overview 
 24 | 
 25 | A basic Model Context Protocol server for keeping and retrieving memories in the OpenSearch engine.
 26 | It acts as a semantic memory layer on top of the OpenSearch database.
 27 | 
 28 | ## Components
 29 | 
 30 | ### Tools
 31 | 
 32 | 1. `search-openSearch`
 33 |    - Store a memory in the OpenSearch database
 34 |    - Input:
 35 |      - `query` (json): prepared json query message
 36 |    - Returns: Confirmation message
 37 | 
 38 | ## Installation
 39 | 
 40 | ### Installing via Smithery
 41 | 
 42 | To install mcp-server-opensearch for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@ibrooksSDX/mcp-server-opensearch):
 43 | 
 44 | ```bash
 45 | npx -y @smithery/cli install @ibrooksSDX/mcp-server-opensearch --client claude
 46 | ```
 47 | 
 48 | ### Using uv (recommended)
 49 | 
 50 | When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed to directly run *mcp-server-opensearch*.
 51 | 
 52 | ```shell
 53 | uv run mcp-server-opensearch \
 54 |   --opensearch-url "http://localhost:9200" \
 55 |   --index-name "my_index" \
 56 | ```
 57 | or 
 58 | 
 59 | ```shell
 60 | uv run fastmcp run demo.py:main
 61 | ```
 62 | 
 63 | ## Testing - Local Open Search Client
 64 | 
 65 | ![image4](./images/osclientTest0.png)
 66 | 
 67 | ```shell
 68 | uv run python src/mcp-server-opensearch/test_opensearch.py
 69 | ```
 70 | ## Testing - MCP Server Connection to Open Search Client
 71 | 
 72 | ![image1](./images/mcpDev0.png)
 73 | ![image2](./images/mcpDev1.png)
 74 | 
 75 | ```shell
 76 | cd src/mcp-server-opensearch
 77 | uv run fastmcp dev demo.py
 78 | ```
 79 | 
 80 | ## Usage with Claude Desktop
 81 | 
 82 | To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
 83 | 
 84 | ```json
 85 | {
 86 |   "opensearch": {
 87 |     "command": "uvx",
 88 |     "args": [
 89 |       "mcp-server-opensearch",
 90 |       "--opensearch-url",
 91 |       "http://localhost:9200",
 92 |       "--opensearch-api-key",
 93 |       "your_api_key",
 94 |       "--index-name",
 95 |       "your_index_name"
 96 |     ]
 97 |   }, "Demo": {
 98 |       "command": "uv",
 99 |       "args": [
100 |         "run",
101 |         "--with",
102 |         "fastmcp",
103 |         "--with",
104 |         "opensearch-py",
105 |         "fastmcp",
106 |         "run",
107 |         "/Users/ibrooks/Documents/GitHub/mcp-server-opensearch/src/mcp-server-opensearch/demo.py"
108 |       ]
109 |     }
110 | }
111 | ```
112 | 
113 | Or use the FastMCP UI to install the server to Claude
114 | 
115 | ```shell
116 | uv run fastmcp install demo.py
117 | ```
118 | 
119 | ## Environment Variables
120 | 
121 | The configuration of the server can be also done using environment variables:
122 | 
123 | - `OPENSEARCH_HOST`: URL of the OpenSearch server, e.g. `http://localhost`
124 | - `OPENSEARCH_HOSTPORT`: Port of the host of the OpenSearch server `9200`
125 | - `INDEX_NAME`: Name of the index to use
126 | 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | from . import server
 2 | import asyncio
 3 | 
 4 | def main():
 5 |     """Main entry point for the package."""
 6 |     asyncio.run(server.main())
 7 | 
 8 | 
 9 | # Optionally expose other important items at package level
10 | __all__ = ["server", "demo"]
11 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-server-opensearch"
 3 | version = "0.1.0"
 4 | description = "MCP server for OpenSearch"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | dependencies = [
 8 |     "anthropic>=0.44.0",
 9 |     "fastmcp>=0.4.1",
10 |     "httpx>=0.28.1",
11 |     "mcp[cli]>=1.2.0",
12 |     "opensearch-py>=2.8.0",
13 |     "python-dotenv>=1.0.1",
14 | ]
15 | 
```

--------------------------------------------------------------------------------
/test_OpenSearchClient.py:
--------------------------------------------------------------------------------

```python
 1 | from opensearchpy import OpenSearch
 2 | 
 3 | host = 'localhost'
 4 | port = 9200
 5 | auth = ('admin', 'pizzaParty123') # For testing only. Don't store credentials in code.
 6 | 
 7 | # Create the client with SSL/TLS and hostname verification disabled.
 8 | client = OpenSearch(
 9 |     hosts = [{'host': host, 'port': port}],
10 |     http_compress = True, # enables gzip compression for request bodies
11 |     http_auth = auth,
12 |     use_ssl = True,
13 |     verify_certs = False,
14 |     ssl_assert_hostname = False,
15 |     ssl_show_warn = False
16 | )
17 | 
18 | q = "Women's Clothing"
19 | query = {
20 |   'size': 5,
21 |   'query': {
22 |     'multi_match': {
23 |       'query': q,
24 |       'fields': ['category']
25 |     }
26 |   }
27 | }
28 | 
29 | response = client.search(
30 |     body = query,
31 |     index = 'opensearch_dashboards_sample_data_ecommerce'
32 | )
33 | 
34 | print('\nSearch results:')
35 | print(response)
36 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - opensearchUrl
10 |       - opensearchHostPort
11 |       - indexName
12 |     properties:
13 |       opensearchUrl:
14 |         type: string
15 |         description: The URL of the OpenSearch server.
16 |       opensearchHostPort:
17 |         type: number
18 |         description: The port of the host of the OpenSearch server.
19 |       indexName:
20 |         type: string
21 |         description: The name of the index to use.
22 |   commandFunction:
23 |     # A function that produces the CLI command to start the MCP on stdio.
24 |     |-
25 |     config => ({command: 'uv', args: ['run', 'mcp-server-opensearch', '--opensearch-url', `${config.opensearchUrl}:${config.opensearchHostPort}`, '--index-name', config.indexName]})
26 | 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/test_opensearch.py:
--------------------------------------------------------------------------------

```python
 1 | import asyncio
 2 | from OpenSearchClient import OpenSearchClient
 3 | import json
 4 | 
 5 | 
 6 | q = "Women's Clothing"
 7 | query = {
 8 |     'size': 5,
 9 |     'query': {
10 |         'multi_match': {
11 |         'query': q,
12 |         'fields': ['category']
13 |         }
14 |     }
15 | }
16 | 
17 | async def test_opensearch():
18 |     # Initialize connector
19 |     connector = OpenSearchClient(
20 |         opensearch_host="localhost",
21 |         opensearch_hostPort=9200,
22 |         index_name="opensearch_dashboards_sample_data_ecommerce",
23 |         bhttp_compress=True,
24 |         buse_ssl=True,
25 |         bverify_certs=False,
26 |         bssl_assert_hostname=False,
27 |         bssl_show_warn=False
28 |     )
29 | 
30 |     # Test search
31 |     try:
32 |         search_results = await connector.search_documents(query)
33 |         print("Search results:", search_results)
34 |     except Exception as e:
35 |         print(f"Error during search: {e}")
36 | 
37 | if __name__ == "__main__":
38 |     asyncio.run(test_opensearch()) 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/test_AsyncClient.py:
--------------------------------------------------------------------------------

```python
 1 | import asyncio
 2 | from AsyncOpenSearchClient import OpenSearchClient
 3 | import json
 4 | 
 5 | 
 6 | q = "Women's Clothing"
 7 | query = {
 8 |     'size': 5,
 9 |     'query': {
10 |         'multi_match': {
11 |         'query': q,
12 |         'fields': ['category']
13 |         }
14 |     }
15 | }
16 | 
17 | async def test_opensearch():
18 |     # Initialize connector
19 |     connector = await OpenSearchClient(
20 |         opensearch_host="localhost",
21 |         opensearch_hostPort=9200,
22 |         index_name="opensearch_dashboards_sample_data_ecommerce",
23 |         bhttp_compress=True,
24 |         buse_ssl=True,
25 |         bverify_certs=False,
26 |         bssl_assert_hostname=False,
27 |         bssl_show_warn=False
28 |     )
29 | 
30 |     # Test search
31 |     try:
32 |         search_results = await connector.search_documents(query)
33 |         print("Search results:", search_results)
34 |     except Exception as e:
35 |         print(f"Error during search: {e}")
36 | 
37 | if __name__ == "__main__":
38 |     asyncio.run(test_opensearch()) 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | # Start with a Python image that includes the uv tool pre-installed
 3 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
 4 | 
 5 | # Set the working directory in the container
 6 | WORKDIR /app
 7 | 
 8 | # Enable bytecode compilation
 9 | ENV UV_COMPILE_BYTECODE=1
10 | 
11 | # Copy the project configuration and lockfile
12 | COPY pyproject.toml uv.lock ./
13 | 
14 | # Install the project's dependencies without installing the project itself
15 | RUN --mount=type=cache,target=/root/.cache/uv     uv sync --frozen --no-install-project --no-dev --no-editable
16 | 
17 | # Add the rest of the project source code
18 | ADD src /app/src
19 | 
20 | # Install the project
21 | RUN --mount=type=cache,target=/root/.cache/uv     uv sync --frozen --no-dev --no-editable
22 | 
23 | # Define the entry point command to run the server
24 | ENTRYPOINT ["uv", "run", "mcp-server-opensearch", "--opensearch-url", "http://localhost:9200", "--index-name", "my_index"]
25 | 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/serverTest.py:
--------------------------------------------------------------------------------

```python
 1 | import logging
 2 | from OpenSearchClient import OpenSearchClient
 3 | from fastmcp import FastMCP
 4 | 
 5 | #from .tools.index import IndexTools
 6 | #from .tools.document import DocumentTools
 7 | #rom .tools.cluster import ClusterTools
 8 | 
 9 | # TESTING VALUES 
10 | opensearch_host="localhost"
11 | opensearch_hostPort=9200
12 | index_name="test_index"
13 | http_compress=True
14 | use_ssl=False
15 | verify_certs=False
16 | ssl_assert_hostname=False
17 | ssl_show_warn=False
18 | 
19 | q = "Women's Clothing"
20 | query = {
21 |     'size': 5,
22 |     'query': {
23 |         'multi_match': {
24 |         'query': q,
25 |         'fields': ['category']
26 |         }
27 |     }
28 | }
29 | ### END OF TESTING VALUES
30 | 
31 | 
32 | class OpenSearchMCPServer:
33 |     def __init__(self):
34 |         self._name = "opensearch_mcp_server"
35 |         self.mcp = FastMCP(self._name)
36 |         
37 |         #Configure and Establish Client to Open Search
38 |         try:
39 |             self._client = OpenSearchClient(opensearch_host, opensearch_hostPort, index_name, http_compress, use_ssl, verify_certs, ssl_assert_hostname, ssl_show_warn)
40 |         except Exception as e:
41 |             print(f"Error during search: {e}")
42 |         
43 |         # Configure logging
44 |         logging.basicConfig(
45 |             level=logging.INFO,
46 |             format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
47 |         )
48 |         self.logger = logging.getLogger(self._name)
49 |         
50 |         # Initialize tools
51 |         self._register_tools()
52 | 
53 |     def _testClient(self):
54 |         self._client.search_documents(query)
55 | 
56 |     def _register_tools(self):
57 |         """Register all MCP tools."""
58 |         # Initialize tool classes
59 |         #index_tools = IndexTools(self.logger)
60 |         #document_tools = DocumentTools(self.logger)
61 |         #cluster_tools = ClusterTools(self.logger)
62 |         
63 |         # Register tools from each module
64 |         #index_tools.register_tools(self.mcp)
65 |         #document_tools.register_tools(self.mcp)
66 |         #cluster_tools.register_tools(self.mcp)
67 | 
68 |     def run(self):
69 |         """Run the MCP server."""
70 |         self.mcp.run()
71 | 
72 | def main():
73 |     server = OpenSearchMCPServer()
74 |     server.run()
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/OpenSearchClient.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import asyncio
 3 | import json
 4 | from fastmcp import FastMCP
 5 | 
 6 | from opensearchpy import OpenSearch
 7 | host = 'localhost'
 8 | port = 9200
 9 | 
10 | def get_string_list_from_json(json_data):
11 |     """Extracts a list of strings from a JSON object."""
12 | 
13 |     if isinstance(json_data, list):
14 |         return [item for item in json_data if isinstance(item, str)]
15 |     elif isinstance(json_data, dict):
16 |         return [value for value in json_data.values() if isinstance(value, str)]
17 |     else:
18 |         return []
19 | 
20 | 
21 | class OpenSearchClient:
22 |     def __init__(
23 |         self,
24 |         opensearch_host: str,
25 |         opensearch_hostPort: str,
26 |         index_name: str,
27 |         bhttp_compress: bool,
28 |         buse_ssl: bool,
29 |         bverify_certs: bool,
30 |         bssl_assert_hostname: bool,
31 |         bssl_show_warn: bool
32 |     ):
33 |         self._index_name = index_name,
34 |         self._host = [{'host':  opensearch_host, 'port': opensearch_hostPort}],
35 |         self._auth =('admin', 'pizzaParty123'),
36 |         self._client = OpenSearch(hosts = [{'host': host, 'port': port}], http_compress = bhttp_compress, http_auth = ('admin', 'pizzaParty123') , use_ssl = buse_ssl, verify_certs = bverify_certs, ssl_assert_hostname = bssl_assert_hostname, ssl_show_warn = bssl_show_warn)
37 |     
38 |     async def search_documents(self, query: str) -> list[str]:
39 |         """
40 |         Find documents in the OpenSearch index. If there are no documents found, an empty list is returned.
41 |         :param query: The query to use for the search.
42 |         :return: A list of documents found.
43 |         """
44 |         index_exists = self._client.indices.exists(index=self._index_name)
45 |         if not index_exists:
46 |            return []
47 |         search_results = self._client.search(
48 |             body=query,
49 |            index = self._index_name
50 |         )
51 |         #search_results_json = json.loads(search_results, indent=4)
52 |         #print(search_results_json)
53 |         #return search_results
54 |     
55 |         return search_results
56 | 
57 |    
58 |     def _return_client(self) -> OpenSearch:
59 |         """Create and return an OpenSearch client using configuration from environment."""
60 |         return self._client
61 | 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/AsyncOpenSearchClient.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import asyncio
 3 | from fastmcp import FastMCP
 4 | 
 5 | from opensearchpy_async import AsyncOpenSearch, AsyncHttpConnection, helpers
 6 | host = 'localhost'
 7 | port = 9200
 8 | 
 9 | def get_string_list_from_json(json_data):
10 |     """Extracts a list of strings from a JSON object."""
11 | 
12 |     if isinstance(json_data, list):
13 |         return [item for item in json_data if isinstance(item, str)]
14 |     elif isinstance(json_data, dict):
15 |         return [value for value in json_data.values() if isinstance(value, str)]
16 |     else:
17 |         return []
18 | 
19 | 
20 | class OpenSearchClient:
21 |     def __init__(
22 |         self,
23 |         opensearch_host: str,
24 |         opensearch_hostPort: str,
25 |         index_name: str,
26 |         bhttp_compress: bool,
27 |         buse_ssl: bool,
28 |         bverify_certs: bool,
29 |         bssl_assert_hostname: bool,
30 |         bssl_show_warn: bool
31 |     ):
32 |         self._index_name = index_name,
33 |         self._host = [{'host':  opensearch_host, 'port': opensearch_hostPort}],
34 |         self._auth =('admin', 'pizzaParty123'),
35 |         self._client = AsyncOpenSearch(hosts = [{'host': host, 'port': port}], http_compress = bhttp_compress, http_auth = ('admin', 'pizzaParty123') , use_ssl = buse_ssl, verify_certs = bverify_certs, ssl_assert_hostname = bssl_assert_hostname, ssl_show_warn = bssl_show_warn)
36 |     
37 |     async def search_documents(self, query: str) -> list[str]:
38 |         """
39 |         Find documents in the OpenSearch index. If there are no documents found, an empty list is returned.
40 |         :param query: The query to use for the search.
41 |         :return: A list of documents found.
42 |         """
43 |         index_exists = self._client.indices.exists(index=self._index_name)
44 |         if not index_exists:
45 |            return []
46 |         search_results = self._client.search(
47 |             body=query,
48 |            index = self._index_name
49 |         )
50 |         #search_results_json = json.loads(search_results, indent=4)
51 |         #print(search_results_json)
52 |         #return search_results
53 |     
54 |         return search_results
55 | 
56 |    
57 |     def _return_client(self) -> AsyncOpenSearch:
58 |         """Create and return an OpenSearch client using configuration from environment."""
59 |         return self._client
60 | 
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/demo.py:
--------------------------------------------------------------------------------

```python
 1 | # demo.py
 2 | 
 3 | from fastmcp import FastMCP
 4 | from OpenSearchClient import OpenSearchClient
 5 | import mcp.types as types
 6 | import asyncio
 7 | import json
 8 | 
 9 | mcp = FastMCP("Demo")
10 | 
11 | SLEEP_DELAY = 2
12 | 
13 | # TESTING VALUES 
14 | T_opensearch_host="localhost"
15 | T_opensearch_hostPort=9200
16 | T_index_name="opensearch_dashboards_sample_data_ecommerce"
17 | T_http_compress=True
18 | T_use_ssl=False
19 | T_verify_certs=False
20 | T_ssl_assert_hostname=False
21 | T_ssl_show_warn=False
22 | 
23 | q = "Women's Clothing"
24 | query = {
25 |     'size': 5,
26 |     'query': {
27 |         'multi_match': {
28 |         'query': "Women's Clothing",
29 |         'fields': ['category']
30 |         }
31 |     }
32 | }
33 | ### END OF TESTING VALUES
34 | 
35 | @mcp.tool()
36 | async def search_openSearch(query) -> list[types.TextContent] :
37 |     client = OpenSearchClient(opensearch_host= T_opensearch_host, opensearch_hostPort=T_opensearch_hostPort, index_name=T_index_name, bhttp_compress = T_http_compress, buse_ssl = T_use_ssl, bverify_certs = T_verify_certs, bssl_assert_hostname = T_ssl_assert_hostname, bssl_show_warn = T_ssl_show_warn)
38 |     queryresult = await client.search_documents(query)
39 | 
40 |     content = [
41 |             types.TextContent(
42 |                 type="text", text=f"Documents for the query '{query}'"
43 |             ),
44 |         ]
45 |     #for doc in queryresult:
46 |     ##       content.append(
47 |     #          types.TextContent(type="text", text=f"<document>{doc}</document>")
48 |     #      )
49 | 
50 |     content.append(types.TextContent(type="text", text=f"<document>{str(queryresult)}</document>"))
51 |     return content
52 | 
53 | 
54 | # Add a dynamic greeting resource
55 | @mcp.resource("greeting://{name}")
56 | def get_greeting(name: str) -> str:
57 |     """Get a personalized greeting"""
58 |     return f"Hello, {name}!"
59 | 
60 | 
61 | def main():
62 |     async def _run():
63 |         async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
64 |             server = serve(search_openSearch)   
65 |             await server.run(
66 |                 read_stream,
67 |                 write_stream,
68 |                 InitializationOptions(
69 |                     server_name="demo",
70 |                     server_version="0.0.1",
71 |                     capabilities=server.get_capabilities(
72 |                         notification_options=NotificationOptions(),
73 |                         experimental_capabilities={},
74 |                     ),
75 |                 ),
76 |             )
77 |             await server.serve_forever()
78 | 
79 |     #asyncio.run(_run())
80 | 
81 | if __name__ == "__main__":
82 |     asyncio.run(main()._run())
```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3'
 2 | services:
 3 |   opensearch-node1: # This is also the hostname of the container within the Docker network (i.e. https://opensearch-node1/)
 4 |     image: opensearchproject/opensearch:latest # Specifying the latest available image - modify if you want a specific version
 5 |     container_name: opensearch-node1
 6 |     environment:
 7 |       - cluster.name=opensearch-cluster # Name the cluster
 8 |       - node.name=opensearch-node1 # Name the node that will run in this container
 9 |       - discovery.seed_hosts=opensearch-node1,opensearch-node2 # Nodes to look for when discovering the cluster
10 |       - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2 # Nodes eligible to serve as cluster manager
11 |       - bootstrap.memory_lock=true # Disable JVM heap memory swapping
12 |       - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # Set min and max JVM heap sizes to at least 50% of system RAM
13 |       - OPENSEARCH_INITIAL_ADMIN_PASSWORD=pizzaParty123    # Sets the demo admin user password when using demo configuration, required for OpenSearch 2.12 and later
14 |     ulimits:
15 |       memlock:
16 |         soft: -1 # Set memlock to unlimited (no soft or hard limit)
17 |         hard: -1
18 |       nofile:
19 |         soft: 65536 # Maximum number of open files for the opensearch user - set to at least 65536
20 |         hard: 65536
21 |     volumes:
22 |       - opensearch-data1:/usr/share/opensearch/data # Creates volume called opensearch-data1 and mounts it to the container
23 |     ports:
24 |       - 9200:9200 # REST API
25 |       - 9600:9600 # Performance Analyzer
26 |     networks:
27 |       - opensearch-net # All of the containers will join the same Docker bridge network
28 |   opensearch-node2:
29 |     image: opensearchproject/opensearch:latest # This should be the same image used for opensearch-node1 to avoid issues
30 |     container_name: opensearch-node2
31 |     environment:
32 |       - cluster.name=opensearch-cluster
33 |       - node.name=opensearch-node2
34 |       - discovery.seed_hosts=opensearch-node1,opensearch-node2
35 |       - cluster.initial_cluster_manager_nodes=opensearch-node1,opensearch-node2
36 |       - bootstrap.memory_lock=true
37 |       - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
38 |       - OPENSEARCH_INITIAL_ADMIN_PASSWORD=pizzaParty123
39 |     ulimits:
40 |       memlock:
41 |         soft: -1
42 |         hard: -1
43 |       nofile:
44 |         soft: 65536
45 |         hard: 65536
46 |     volumes:
47 |       - opensearch-data2:/usr/share/opensearch/data
48 |     networks:
49 |       - opensearch-net
50 |   opensearch-dashboards:
51 |     image: opensearchproject/opensearch-dashboards:latest # Make sure the version of opensearch-dashboards matches the version of opensearch installed on other nodes
52 |     container_name: opensearch-dashboards
53 |     ports:
54 |       - 5601:5601 # Map host port 5601 to container port 5601
55 |     expose:
56 |       - "5601" # Expose port 5601 for web access to OpenSearch Dashboards
57 |     environment:
58 |       OPENSEARCH_HOSTS: '["https://opensearch-node1:9200","https://opensearch-node2:9200"]' # Define the OpenSearch nodes that OpenSearch Dashboards will query
59 |     networks:
60 |       - opensearch-net
61 | 
62 | volumes:
63 |   opensearch-data1:
64 |   opensearch-data2:
65 | 
66 | networks:
67 |   opensearch-net:
```

--------------------------------------------------------------------------------
/src/mcp-server-opensearch/server.py:
--------------------------------------------------------------------------------

```python
  1 | import httpx
  2 | import click
  3 | import asyncio
  4 | 
  5 | #from mcp.server.lowlevel import Server, NotificationOptions
  6 | from mcp.server.models import InitializationOptions
  7 | import mcp.server.stdio
  8 | import mcp.types as types
  9 | from mcp.server.fastmcp import FastMCP
 10 | from mcp.server import Server
 11 | 
 12 | 
 13 | 
 14 | from OpenSearchClient import OpenSearchClient
 15 | 
 16 | # TESTING VALUES 
 17 | T_opensearch_host="localhost"
 18 | T_opensearch_hostPort=9200
 19 | T_index_name="opensearch_dashboards_sample_data_ecommerce"
 20 | T_http_compress=True
 21 | T_use_ssl=False
 22 | T_verify_certs=False
 23 | T_ssl_assert_hostname=False
 24 | T_ssl_show_warn=False
 25 | 
 26 | q = "Women's Clothing"
 27 | query = {
 28 |     'size': 5,
 29 |     'query': {
 30 |         'multi_match': {
 31 |         'query': q,
 32 |         'fields': ['category']
 33 |         }
 34 |     }
 35 | }
 36 | ### END OF TESTING VALUES
 37 | 
 38 | def serve(
 39 |     opensearch_host: str,
 40 |     opensearch_hostPort: str,
 41 |     index_name: str,
 42 |     http_compress: bool,
 43 |     use_ssl: bool,
 44 |     verify_certs: bool,
 45 |     ssl_assert_hostname: bool,
 46 |     ssl_show_warn: bool
 47 | ) -> Server:
 48 |     """
 49 |     Instantiate the server and configure tools to store and find documents in OpenSearch.
 50 |     :param opensearch_host: The URL of the OpenSearch server.
 51 |     :param opensearch_hostPort: The port number of the OpenSearch server.
 52 |     :param index_name: The name of the index to use.
 53 |     """
 54 |     serverAPP = FastMCP("opensearch")
 55 | 
 56 |     opensearch = OpenSearchClient(
 57 |        opensearch_host = T_opensearch_host, opensearch_hostPort = T_opensearch_hostPort, index_name = T_index_name, bhttp_compress=T_http_compress, buse_ssl=T_use_ssl, bverify_certs=T_verify_certs, bssl_assert_hostname=T_ssl_assert_hostname, bssl_show_warn=T_ssl_show_warn
 58 |     )
 59 | 
 60 |     @serverAPP.list_tools()
 61 |     async def handle_list_tools() -> list[types.Tool]:
 62 |         """
 63 |         Return the list of tools that the server provides. By default, there are two
 64 |         tools: one to store documents and another to find them. 
 65 |         """
 66 |         return [
 67 |             types.Tool(
 68 |                 name="opensearch-find-documents",
 69 |                 description=(
 70 |                     "Look up documents in OpenSearch. Use this tool when you need to: \n"
 71 |                     " - Find documents by their index or content"
 72 |                 ),
 73 |                 inputSchema={
 74 |                     "type": "object",
 75 |                     "properties": {
 76 |                         "query": {
 77 |                             "type": "string",
 78 |                             "description": "The query to search for in the documents",
 79 |                         },
 80 |                     },
 81 |                     "required": ["query"],
 82 |                 },
 83 |             ),
 84 |         ]
 85 | 
 86 |     @serverAPP.call_tool()
 87 |     async def handle_tool_call(
 88 |         name: str, arguments: dict | None
 89 |     ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
 90 |         if name not in ["opensearch-find-documents"]:
 91 |             raise ValueError(f"Unknown tool: {name}")
 92 | 
 93 |         if name == "opensearch-find-documents":
 94 |             if not arguments or "query" not in arguments:
 95 |                 raise ValueError("Missing required argument 'query'")
 96 |             query = arguments["query"]
 97 |             documents = await opensearch.search_documents(query)
 98 |             content = [
 99 |                 types.TextContent(
100 |                     type="text", text=f"Documents for the query '{query}'"
101 |                 ),
102 |             ]
103 |             for doc in documents:
104 |                 content.append(
105 |                     types.TextContent(type="text", text=f"<document>{doc}</document>")
106 |                 )
107 |             return content
108 | 
109 |     return serverAPP
110 | 
111 | 
112 | @click.command()
113 | @click.option(
114 |     "--opensearch_host",
115 |     envvar="OPENSEARCH_HOST",
116 |     required=True,
117 |     help="Open Search Host URL",
118 | )
119 | @click.option(
120 |     "--opensearch_hostPort",
121 |     envvar="OPENSEARCH_HOST_PORT",
122 |     required=True,
123 |     help="Open Search Port Number",
124 | )
125 | @click.option(
126 |     "--index-name",
127 |     envvar="INDEX_NAME",
128 |     required=True,
129 |     help="Index name",
130 | )
131 | def main(
132 |     opensearch_host: str,
133 |     opensearch_hostPort: str,
134 |     index_name: str,
135 |     http_compress: bool,
136 |     use_ssl: bool,
137 |     verify_certs: bool,
138 |     ssl_assert_hostname: bool,
139 |     ssl_show_warn: bool
140 | ):
141 |     async def _run():
142 |         async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
143 |             server = serve(
144 |                 opensearch_host,
145 |                 opensearch_hostPort,
146 |                 index_name,
147 |                 http_compress,
148 |                 use_ssl,
149 |                 verify_certs,
150 |                 ssl_assert_hostname,
151 |                 ssl_show_warn
152 |             )
153 |             await server.run(
154 |                 read_stream,
155 |                write_stream,
156 |                InitializationOptions(
157 |                 server_name="openSearch",
158 |                     server_version="0.1.0",
159 |                     capabilities=server.get_capabilities(
160 |                        notification_options=NotificationOptions(),
161 |                        experimental_capabilities={},
162 |                    ),
163 |                 ),
164 |             )
165 |     asyncio.run(_run())
```