Skip to content

Commit 4621aa1

Browse files
committed
chore: fix merge conflicts
2 parents d2dfe7b + 60cc8c6 commit 4621aa1

17 files changed

+550
-132
lines changed

.env.example

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Elasticsearch connection settings
2+
ELASTICSEARCH_HOSTS=https://localhost:9200
3+
ELASTICSEARCH_USERNAME=elastic
4+
ELASTICSEARCH_PASSWORD=test123
5+
6+
# OpenSearch connection settings
7+
OPENSEARCH_HOSTS=https://localhost:9200
8+
OPENSEARCH_USERNAME=admin
9+
OPENSEARCH_PASSWORD=admin

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# IDE
12
.idea
23
.vscode
4+
5+
# Python
36
.venv
47
dist
58
__pycache__
69
*.egg-info
10+
11+
# Configuration and Credentials
12+
.env

README.md

+68-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Elasticsearch MCP Server
1+
# Elasticsearch/OpenSearch MCP Server
22

33
[![smithery badge](https://smithery.ai/badge/elasticsearch-mcp-server)](https://smithery.ai/server/elasticsearch-mcp-server)
44

55
## Overview
66

7-
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.
7+
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.
88

99
<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>
1010

@@ -16,30 +16,41 @@ https://github.com/user-attachments/assets/f7409e31-fac4-4321-9c94-b0ff2ea7ff15
1616

1717
### Index Operations
1818

19-
- `list_indices`: List all indices in the Elasticsearch cluster.
20-
- `get_mapping`: Retrieve the mapping configuration for a specific index.
21-
- `get_settings`: Get the settings configuration for a specific index.
19+
- `list_indices`: List all indices.
20+
- `get_index`: Returns information (mappings, settings, aliases) about one or more indices.
21+
- `create_index`: Create a new index.
22+
- `delete_index`: Delete an index.
2223

2324
### Document Operations
2425

25-
- `search_documents`: Search documents in an index using Elasticsearch Query DSL.
26+
- `search_documents`: Search for documents.
27+
- `index_document`: Creates or updates a document in the index.
28+
- `get_document`: Get a document by ID.
29+
- `delete_document`: Delete a document by ID.
30+
- `delete_by_query`: Deletes documents matching the provided query.
2631

2732
### Cluster Operations
2833

29-
- `get_cluster_health`: Get health status of the cluster.
30-
- `get_cluster_stats`: Get statistical information about the cluster.
34+
- `get_cluster_health`: Returns basic information about the health of the cluster.
35+
- `get_cluster_stats`: Returns high-level overview of cluster statistics.
3136

37+
### Alias Operations
38+
39+
- `list_aliases`: List all aliases.
40+
- `get_alias`: Get alias information for a specific index.
41+
- `put_alias`: Create or update an alias for a specific index.
42+
- `delete_alias`: Delete an alias for a specific index.
3243

3344
## Start Elasticsearch/OpenSearch Cluster
3445

3546
Start the Elasticsearch/OpenSearch cluster using Docker Compose:
3647

3748
```bash
3849
# For Elasticsearch
39-
docker-compose -f setup/elasticsearch-docker-compose.yml up -d
50+
docker-compose -f docker-compose-elasticsearch.yml up -d
4051

4152
# For OpenSearch
42-
docker-compose -f setup/opensearch-docker-compose.yml up -d
53+
docker-compose -f docker-compose-opensearch.yml up -d
4354
```
4455

4556
The default Elasticsearch username is `elastic` and password is `test123`. The default OpenSearch username is `admin` and password is `admin`.
@@ -61,6 +72,7 @@ npx -y @smithery/cli install elasticsearch-mcp-server --client claude
6172
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`.
6273

6374
```json
75+
// For Elasticsearch
6476
{
6577
"mcpServers": {
6678
"elasticsearch-mcp-server": {
@@ -76,16 +88,34 @@ Using `uvx` will automatically install the package from PyPI, no need to clone t
7688
}
7789
}
7890
}
91+
92+
// For OpenSearch
93+
{
94+
"mcpServers": {
95+
"opensearch-mcp-server": {
96+
"command": "uvx",
97+
"args": [
98+
"opensearch-mcp-server"
99+
],
100+
"env": {
101+
"OPENSEARCH_HOST": "https://localhost:9200",
102+
"OPENSEARCH_USERNAME": "admin",
103+
"OPENSEARCH_PASSWORD": "admin"
104+
}
105+
}
106+
}
107+
}
79108
```
80109

81110
### Option 3: Using uv with local development
82111

83112
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`.
84113

85114
```json
115+
// For Elasticsearch
86116
{
87117
"mcpServers": {
88-
"elasticsearch": {
118+
"elasticsearch-mcp-server": {
89119
"command": "uv",
90120
"args": [
91121
"--directory",
@@ -101,18 +131,44 @@ Using `uv` requires cloning the repository locally and specifying the path to th
101131
}
102132
}
103133
}
134+
135+
// For OpenSearch
136+
{
137+
"mcpServers": {
138+
"opensearch-mcp-server": {
139+
"command": "uv",
140+
"args": [
141+
"--directory",
142+
"path/to/src/elasticsearch_mcp_server",
143+
"run",
144+
"opensearch-mcp-server"
145+
],
146+
"env": {
147+
"OPENSEARCH_HOST": "https://localhost:9200",
148+
"OPENSEARCH_USERNAME": "admin",
149+
"OPENSEARCH_PASSWORD": "admin"
150+
}
151+
}
152+
}
153+
}
104154
```
105155

106156
- On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
107157
- On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
108158

109159
Restart Claude Desktop to load the new MCP server.
110160

111-
Now you can interact with your Elasticsearch cluster through Claude using natural language commands like:
161+
Now you can interact with your Elasticsearch/OpenSearch cluster through Claude using natural language commands like:
112162
- "List all indices in the cluster"
113163
- "How old is the student Bob?"
114164
- "Show me the cluster health status"
115165

166+
## Usage with Anthropic MCP Client
167+
168+
```python
169+
uv run mcp_client/client.py src/server.py
170+
```
171+
116172
## License
117173

118174
This project is licensed under the Apache License Version 2.0 - see the [LICENSE](LICENSE) file for details.

cliff.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,4 @@ git_path = "git"
9797
# whether to use relaxed or strict semver parsing
9898
relaxed_semver = true
9999
# only show the changes for the current version
100-
tag_range = true
100+
tag_range = true

setup/elasticsearch-docker-compose.yml docker-compose-elasticsearch.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ services:
66
user: "0"
77
command: >
88
bash -c '
9-
if [ x${ELASTIC_PASSWORD} == x ]; then
10-
echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
9+
if [ x${ELASTICSEARCH_PASSWORD} == x ]; then
10+
echo "Set the ELASTICSEARCH_PASSWORD environment variable in the .env file";
1111
exit 1;
1212
fi;
1313
if [ ! -f config/certs/ca.zip ]; then
@@ -48,7 +48,7 @@ services:
4848
echo "Waiting for Elasticsearch availability";
4949
until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
5050
echo "Setting kibana_system password";
51-
until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"kibana123\"}" | grep -q "^{}"; do sleep 10; done;
51+
until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTICSEARCH_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"kibana123\"}" | grep -q "^{}"; do sleep 10; done;
5252
echo "All done!";
5353
'
5454
healthcheck:
@@ -72,7 +72,7 @@ services:
7272
- cluster.name=es-mcp-cluster
7373
- cluster.initial_master_nodes=es01,es02,es03
7474
- discovery.seed_hosts=es02,es03
75-
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
75+
- ELASTIC_PASSWORD=${ELASTICSEARCH_PASSWORD}
7676
- bootstrap.memory_lock=true
7777
- xpack.security.enabled=true
7878
- xpack.security.http.ssl.enabled=true
File renamed without changes.

mcp_client/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

mcp_client/__init__.py

Whitespace-only changes.

mcp_client/client.py

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""
2+
Client example copied from https://modelcontextprotocol.io/quickstart/client
3+
"""
4+
5+
import asyncio
6+
from typing import Optional
7+
from contextlib import AsyncExitStack
8+
9+
from mcp import ClientSession, StdioServerParameters
10+
from mcp.client.stdio import stdio_client
11+
12+
from anthropic import Anthropic
13+
14+
from config import get_logger, read_config
15+
16+
logger = get_logger(__name__)
17+
18+
19+
class MCPClient:
20+
def __init__(self):
21+
self.session: Optional[ClientSession] = None
22+
self.exit_stack = AsyncExitStack()
23+
self.anthropic = Anthropic()
24+
self.config = read_config()
25+
26+
async def connect_to_server(self, server_script_path: str):
27+
"""Connect to an MCP server
28+
29+
Args:
30+
server_script_path: Path to the server script (.py or .js)
31+
"""
32+
is_python = server_script_path.endswith('.py')
33+
is_js = server_script_path.endswith('.js')
34+
if not (is_python or is_js):
35+
raise ValueError("Server script must be a .py or .js file")
36+
37+
command = "python" if is_python else "node"
38+
server_params = StdioServerParameters(command=command, args=[server_script_path], env=None)
39+
40+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
41+
self.stdio, self.write = stdio_transport
42+
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
43+
44+
if self.session is not None:
45+
await self.session.initialize()
46+
response = await self.session.list_tools()
47+
tools = response.tools
48+
logger.info(f"\nConnected to server with tools: {', '.join(tool.name for tool in tools)}")
49+
50+
async def process_query(self, query: str) -> str:
51+
"""Process a query using Claude and available tools"""
52+
messages = [
53+
{
54+
"role": "user",
55+
"content": query
56+
}
57+
]
58+
59+
response = await self.session.list_tools()
60+
available_tools = [{
61+
"name": tool.name,
62+
"description": tool.description,
63+
"input_schema": tool.inputSchema
64+
} for tool in response.tools]
65+
66+
# Initial Claude API call
67+
response = self.anthropic.messages.create(
68+
model=self.config.anthropic.model,
69+
max_tokens=self.config.anthropic.max_tokens_message,
70+
messages=messages,
71+
tools=available_tools
72+
)
73+
74+
# Process response and handle tool calls
75+
final_text = []
76+
77+
assistant_message_content = []
78+
for content in response.content:
79+
if content.type == 'text':
80+
final_text.append(content.text)
81+
assistant_message_content.append(content)
82+
elif content.type == 'tool_use':
83+
tool_name = content.name
84+
tool_args = content.input
85+
86+
# Execute tool call
87+
result = await self.session.call_tool(tool_name, tool_args)
88+
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
89+
90+
assistant_message_content.append(content)
91+
messages.append({
92+
"role": "assistant",
93+
"content": assistant_message_content
94+
})
95+
messages.append({
96+
"role": "user",
97+
"content": [
98+
{
99+
"type": "tool_result",
100+
"tool_use_id": content.id,
101+
"content": result.content
102+
}
103+
]
104+
})
105+
106+
# Get next response from Claude
107+
response = self.anthropic.messages.create(
108+
model=self.config.anthropic.model,
109+
max_tokens=self.config.anthropic.max_tokens_message,
110+
messages=messages,
111+
tools=available_tools
112+
)
113+
114+
final_text.append(response.content[0].text)
115+
116+
return "\n".join(final_text)
117+
118+
async def chat_loop(self):
119+
"""Run an interactive chat loop"""
120+
logger.info("\nMCP Client Started!")
121+
logger.info("Type your queries or 'quit' to exit.")
122+
123+
while True:
124+
try:
125+
query = input("\nQuery: ").strip()
126+
127+
if query.lower() == 'quit':
128+
break
129+
130+
response = await self.process_query(query)
131+
logger.info("\n" + response)
132+
133+
except Exception as e:
134+
logger.error(f"\nError: {str(e)}")
135+
136+
async def cleanup(self):
137+
"""Clean up resources"""
138+
await self.exit_stack.aclose()
139+
140+
141+
async def main():
142+
# if len(sys.argv) < 2:
143+
# logger.exception("Usage: python client.py <path_to_server_script>")
144+
# return
145+
146+
client = MCPClient()
147+
try:
148+
await client.connect_to_server(sys.argv[1])
149+
logger.info(f"Connected to the server: {sys.argv[1]}.")
150+
await client.chat_loop()
151+
finally:
152+
await client.cleanup()
153+
logger.info(f"Disconnected from the server: {sys.argv[1]}.")
154+
155+
156+
if __name__ == "__main__":
157+
import sys
158+
asyncio.run(main())

0 commit comments

Comments
 (0)