# 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 | [](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 | 
11 | 
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 | 
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 | 
73 | 
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())
```