# 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()) ```