artisan-es-reader-plugin 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- artisan_es_reader_plugin-0.4.0/PKG-INFO +179 -0
- artisan_es_reader_plugin-0.4.0/README.md +167 -0
- artisan_es_reader_plugin-0.4.0/mcp-config.example.json +8 -0
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/pyproject.toml +1 -1
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/src/es_mcp/server.py +218 -35
- artisan_es_reader_plugin-0.3.0/PKG-INFO +0 -153
- artisan_es_reader_plugin-0.3.0/README.md +0 -141
- artisan_es_reader_plugin-0.3.0/mcp-config.example.json +0 -24
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/.env.example +0 -0
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/.gitignore +0 -0
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/artisan-es-reader-plugin.plugin +0 -0
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/requirements.txt +0 -0
- {artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/src/es_mcp/__init__.py +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: artisan-es-reader-plugin
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Elasticsearch MCP server — query logs with or without SSH tunnel
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: elasticsearch<9.0.0,>=8.0.0
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
Requires-Dist: paramiko>=3.0.0
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
+
Requires-Dist: sshtunnel>=0.4.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Elasticsearch MCP Server
|
|
14
|
+
|
|
15
|
+
Query Elasticsearch logs directly from Claude Cowork — with or without an SSH tunnel.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### 1. Install the plugin
|
|
22
|
+
|
|
23
|
+
Get **es-mcp** from the Cowork plugin marketplace and install it.
|
|
24
|
+
|
|
25
|
+
### 2. Install `uv`
|
|
26
|
+
|
|
27
|
+
The MCP server runs via `uvx`, which requires `uv` to be installed on your machine:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
# Windows
|
|
31
|
+
powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# macOS / Linux
|
|
36
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Add the MCP entry to claude_desktop_config.json
|
|
40
|
+
|
|
41
|
+
Open the file at:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
45
|
+
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Add this minimal entry inside `"mcpServers": { }`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"elasticsearch-logs": {
|
|
54
|
+
"command": "uvx",
|
|
55
|
+
"args": ["artisan-es-reader-plugin@latest", "artisan-es-reader-plugin"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That's it — no env vars needed.
|
|
62
|
+
|
|
63
|
+
### 4. Configure your profiles by asking Claude
|
|
64
|
+
|
|
65
|
+
Restart Cowork, then just ask Claude to add your Elasticsearch connections:
|
|
66
|
+
|
|
67
|
+
> "Add a tealive_production profile — bastion is 43.216.208.205, pem is at ~/keys/prod.pem"
|
|
68
|
+
|
|
69
|
+
Claude will call `configure_profile` and save your settings to `~/.es-mcp/profiles.json`
|
|
70
|
+
automatically. You can add as many profiles as you like this way.
|
|
71
|
+
|
|
72
|
+
### 5. Restart Cowork
|
|
73
|
+
|
|
74
|
+
The SSH tunnel starts automatically on first tool use.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## How profiles are stored
|
|
79
|
+
|
|
80
|
+
Profiles are saved to `~/.es-mcp/profiles.json` — a plain, human-readable JSON file
|
|
81
|
+
you can also edit directly if you prefer:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"default": "tealive_production",
|
|
86
|
+
"profiles": {
|
|
87
|
+
"tealive_production": {
|
|
88
|
+
"es_use_ssl": true,
|
|
89
|
+
"es_verify_certs": false,
|
|
90
|
+
"es_username": "your-username",
|
|
91
|
+
"es_password": "your-password",
|
|
92
|
+
"ssh_host": "43.216.208.205",
|
|
93
|
+
"ssh_username": "ubuntu",
|
|
94
|
+
"ssh_pem_file": "~/keys/prod.pem"
|
|
95
|
+
},
|
|
96
|
+
"baskbear": {
|
|
97
|
+
"es_use_ssl": true,
|
|
98
|
+
"es_verify_certs": false,
|
|
99
|
+
"ssh_host": "baskbear-bastion-ip",
|
|
100
|
+
"ssh_username": "ubuntu",
|
|
101
|
+
"ssh_pem_file": "~/keys/baskbear.pem"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Config priority order
|
|
108
|
+
|
|
109
|
+
The server looks for profiles in this order — the first match wins:
|
|
110
|
+
|
|
111
|
+
1. `ES_PROFILES` env var (JSON string) — backward compat for existing users
|
|
112
|
+
2. `ES_PROFILES_FILE` env var — path to a custom JSON file
|
|
113
|
+
3. `~/.es-mcp/profiles.json` — auto-discovered (written by `configure_profile`)
|
|
114
|
+
4. Legacy flat `ES_*` / `SSH_*` env vars — single profile named "default"
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Selecting a profile
|
|
119
|
+
|
|
120
|
+
Every tool accepts an optional `profile` argument. Just ask naturally:
|
|
121
|
+
|
|
122
|
+
> "Search **baskbear** logs for payment errors"
|
|
123
|
+
> "Show recent errors in **tealive production**"
|
|
124
|
+
|
|
125
|
+
Omit it and the `default` profile is used. Call `list_profiles` to see what's
|
|
126
|
+
configured, or `connection_info` to inspect a specific one.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Per-profile settings reference
|
|
131
|
+
|
|
132
|
+
| Key | Description | Default |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `es_host` / `es_port` | ES host/port for **direct** connections (ignored when `ssh_host` is set) | `localhost` / `9200` |
|
|
135
|
+
| `es_username` / `es_password` | Elasticsearch credentials | empty |
|
|
136
|
+
| `es_use_ssl` | `true` if ES runs HTTPS | `false` |
|
|
137
|
+
| `es_verify_certs` | `false` for self-signed certs | `false` |
|
|
138
|
+
| `ssh_host` | Bastion / jump host IP or hostname. Leave unset for a direct connection | empty |
|
|
139
|
+
| `ssh_port` / `ssh_username` | SSH port / user | `22` / `ubuntu` |
|
|
140
|
+
| `ssh_pem_file` | Path to the PEM key on this machine (e.g. `~/keys/prod.pem`) | `~/.ssh/id_rsa` |
|
|
141
|
+
| `ssh_remote_es_host` / `ssh_remote_es_port` | ES host/port as seen from the bastion | `localhost` / `9200` |
|
|
142
|
+
| `ssh_local_port` | Local tunnel port (`0` = auto) | `0` |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Available tools
|
|
147
|
+
|
|
148
|
+
All tools accept an optional `profile` argument to choose the Elasticsearch source.
|
|
149
|
+
|
|
150
|
+
| Tool | What it does |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `configure_profile` | Add or update a profile — saves to `~/.es-mcp/profiles.json` |
|
|
153
|
+
| `delete_profile` | Remove a profile |
|
|
154
|
+
| `list_profiles` | List configured profiles and the default |
|
|
155
|
+
| `list_indices` | List indices (supports glob pattern) |
|
|
156
|
+
| `search_logs` | Full-text search with filters, sort, pagination |
|
|
157
|
+
| `get_recent_errors` | Error-level entries from the last N minutes |
|
|
158
|
+
| `get_index_mapping` | Field schema for an index |
|
|
159
|
+
| `run_aggregation` | Run a custom ES aggregation |
|
|
160
|
+
| `connection_info` | Show active connection / tunnel status |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Updating
|
|
165
|
+
|
|
166
|
+
### For users
|
|
167
|
+
|
|
168
|
+
Updates are automatic — just restart Cowork and `uvx` will pull the latest version from PyPI.
|
|
169
|
+
|
|
170
|
+
### For maintainers
|
|
171
|
+
|
|
172
|
+
1. Make changes to `src/es_mcp/server.py`
|
|
173
|
+
2. Bump the version in `pyproject.toml`
|
|
174
|
+
3. Build and publish:
|
|
175
|
+
```bash
|
|
176
|
+
python -m build
|
|
177
|
+
twine upload dist/*
|
|
178
|
+
```
|
|
179
|
+
4. Users get the new version automatically on next Cowork restart — no action needed on their end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Elasticsearch MCP Server
|
|
2
|
+
|
|
3
|
+
Query Elasticsearch logs directly from Claude Cowork — with or without an SSH tunnel.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### 1. Install the plugin
|
|
10
|
+
|
|
11
|
+
Get **es-mcp** from the Cowork plugin marketplace and install it.
|
|
12
|
+
|
|
13
|
+
### 2. Install `uv`
|
|
14
|
+
|
|
15
|
+
The MCP server runs via `uvx`, which requires `uv` to be installed on your machine:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
# Windows
|
|
19
|
+
powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# macOS / Linux
|
|
24
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Add the MCP entry to claude_desktop_config.json
|
|
28
|
+
|
|
29
|
+
Open the file at:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
33
|
+
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add this minimal entry inside `"mcpServers": { }`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"elasticsearch-logs": {
|
|
42
|
+
"command": "uvx",
|
|
43
|
+
"args": ["artisan-es-reader-plugin@latest", "artisan-es-reader-plugin"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it — no env vars needed.
|
|
50
|
+
|
|
51
|
+
### 4. Configure your profiles by asking Claude
|
|
52
|
+
|
|
53
|
+
Restart Cowork, then just ask Claude to add your Elasticsearch connections:
|
|
54
|
+
|
|
55
|
+
> "Add a tealive_production profile — bastion is 43.216.208.205, pem is at ~/keys/prod.pem"
|
|
56
|
+
|
|
57
|
+
Claude will call `configure_profile` and save your settings to `~/.es-mcp/profiles.json`
|
|
58
|
+
automatically. You can add as many profiles as you like this way.
|
|
59
|
+
|
|
60
|
+
### 5. Restart Cowork
|
|
61
|
+
|
|
62
|
+
The SSH tunnel starts automatically on first tool use.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## How profiles are stored
|
|
67
|
+
|
|
68
|
+
Profiles are saved to `~/.es-mcp/profiles.json` — a plain, human-readable JSON file
|
|
69
|
+
you can also edit directly if you prefer:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"default": "tealive_production",
|
|
74
|
+
"profiles": {
|
|
75
|
+
"tealive_production": {
|
|
76
|
+
"es_use_ssl": true,
|
|
77
|
+
"es_verify_certs": false,
|
|
78
|
+
"es_username": "your-username",
|
|
79
|
+
"es_password": "your-password",
|
|
80
|
+
"ssh_host": "43.216.208.205",
|
|
81
|
+
"ssh_username": "ubuntu",
|
|
82
|
+
"ssh_pem_file": "~/keys/prod.pem"
|
|
83
|
+
},
|
|
84
|
+
"baskbear": {
|
|
85
|
+
"es_use_ssl": true,
|
|
86
|
+
"es_verify_certs": false,
|
|
87
|
+
"ssh_host": "baskbear-bastion-ip",
|
|
88
|
+
"ssh_username": "ubuntu",
|
|
89
|
+
"ssh_pem_file": "~/keys/baskbear.pem"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Config priority order
|
|
96
|
+
|
|
97
|
+
The server looks for profiles in this order — the first match wins:
|
|
98
|
+
|
|
99
|
+
1. `ES_PROFILES` env var (JSON string) — backward compat for existing users
|
|
100
|
+
2. `ES_PROFILES_FILE` env var — path to a custom JSON file
|
|
101
|
+
3. `~/.es-mcp/profiles.json` — auto-discovered (written by `configure_profile`)
|
|
102
|
+
4. Legacy flat `ES_*` / `SSH_*` env vars — single profile named "default"
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Selecting a profile
|
|
107
|
+
|
|
108
|
+
Every tool accepts an optional `profile` argument. Just ask naturally:
|
|
109
|
+
|
|
110
|
+
> "Search **baskbear** logs for payment errors"
|
|
111
|
+
> "Show recent errors in **tealive production**"
|
|
112
|
+
|
|
113
|
+
Omit it and the `default` profile is used. Call `list_profiles` to see what's
|
|
114
|
+
configured, or `connection_info` to inspect a specific one.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Per-profile settings reference
|
|
119
|
+
|
|
120
|
+
| Key | Description | Default |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `es_host` / `es_port` | ES host/port for **direct** connections (ignored when `ssh_host` is set) | `localhost` / `9200` |
|
|
123
|
+
| `es_username` / `es_password` | Elasticsearch credentials | empty |
|
|
124
|
+
| `es_use_ssl` | `true` if ES runs HTTPS | `false` |
|
|
125
|
+
| `es_verify_certs` | `false` for self-signed certs | `false` |
|
|
126
|
+
| `ssh_host` | Bastion / jump host IP or hostname. Leave unset for a direct connection | empty |
|
|
127
|
+
| `ssh_port` / `ssh_username` | SSH port / user | `22` / `ubuntu` |
|
|
128
|
+
| `ssh_pem_file` | Path to the PEM key on this machine (e.g. `~/keys/prod.pem`) | `~/.ssh/id_rsa` |
|
|
129
|
+
| `ssh_remote_es_host` / `ssh_remote_es_port` | ES host/port as seen from the bastion | `localhost` / `9200` |
|
|
130
|
+
| `ssh_local_port` | Local tunnel port (`0` = auto) | `0` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Available tools
|
|
135
|
+
|
|
136
|
+
All tools accept an optional `profile` argument to choose the Elasticsearch source.
|
|
137
|
+
|
|
138
|
+
| Tool | What it does |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `configure_profile` | Add or update a profile — saves to `~/.es-mcp/profiles.json` |
|
|
141
|
+
| `delete_profile` | Remove a profile |
|
|
142
|
+
| `list_profiles` | List configured profiles and the default |
|
|
143
|
+
| `list_indices` | List indices (supports glob pattern) |
|
|
144
|
+
| `search_logs` | Full-text search with filters, sort, pagination |
|
|
145
|
+
| `get_recent_errors` | Error-level entries from the last N minutes |
|
|
146
|
+
| `get_index_mapping` | Field schema for an index |
|
|
147
|
+
| `run_aggregation` | Run a custom ES aggregation |
|
|
148
|
+
| `connection_info` | Show active connection / tunnel status |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Updating
|
|
153
|
+
|
|
154
|
+
### For users
|
|
155
|
+
|
|
156
|
+
Updates are automatic — just restart Cowork and `uvx` will pull the latest version from PyPI.
|
|
157
|
+
|
|
158
|
+
### For maintainers
|
|
159
|
+
|
|
160
|
+
1. Make changes to `src/es_mcp/server.py`
|
|
161
|
+
2. Bump the version in `pyproject.toml`
|
|
162
|
+
3. Build and publish:
|
|
163
|
+
```bash
|
|
164
|
+
python -m build
|
|
165
|
+
twine upload dist/*
|
|
166
|
+
```
|
|
167
|
+
4. Users get the new version automatically on next Cowork restart — no action needed on their end
|
|
@@ -4,12 +4,23 @@ Supports direct connection or SSH tunnel (PEM file).
|
|
|
4
4
|
|
|
5
5
|
Multiple Elasticsearch sources ("profiles") can be configured at once — e.g.
|
|
6
6
|
tealive staging, tealive production, baskbear — and selected per tool call via
|
|
7
|
-
the optional `profile` argument.
|
|
7
|
+
the optional `profile` argument.
|
|
8
|
+
|
|
9
|
+
## Profile config (priority order)
|
|
10
|
+
|
|
11
|
+
1. ES_PROFILES env var — JSON string (backward compat, existing users)
|
|
12
|
+
2. ES_PROFILES_FILE env var — path to a JSON file
|
|
13
|
+
3. Auto-discovered file — ~/.es-mcp/profiles.json (created by configure_profile)
|
|
14
|
+
4. Legacy flat env vars — ES_HOST, SSH_HOST, etc. (single profile named "default")
|
|
15
|
+
|
|
16
|
+
The easiest way to configure profiles is to ask Claude to run configure_profile —
|
|
17
|
+
it writes ~/.es-mcp/profiles.json so you never have to touch claude_desktop_config.json.
|
|
8
18
|
"""
|
|
9
19
|
|
|
10
|
-
import os
|
|
11
20
|
import json
|
|
21
|
+
import os
|
|
12
22
|
import paramiko
|
|
23
|
+
from pathlib import Path
|
|
13
24
|
from typing import Any
|
|
14
25
|
|
|
15
26
|
# sshtunnel references the removed paramiko.DSSKey in older versions — patch before import
|
|
@@ -24,19 +35,15 @@ from sshtunnel import SSHTunnelForwarder
|
|
|
24
35
|
load_dotenv()
|
|
25
36
|
|
|
26
37
|
# ---------------------------------------------------------------------------
|
|
27
|
-
#
|
|
38
|
+
# Default profiles file location
|
|
28
39
|
# ---------------------------------------------------------------------------
|
|
29
|
-
#
|
|
30
|
-
# A "profile" is one named Elasticsearch source with its own connection +
|
|
31
|
-
# (optional) SSH tunnel settings. Profiles are declared via the ES_PROFILES
|
|
32
|
-
# env var (JSON). Two accepted shapes:
|
|
33
|
-
#
|
|
34
|
-
# {"default": "tealive_staging", "profiles": {"tealive_staging": {...}, ...}}
|
|
35
|
-
# {"tealive_staging": {...}, "tealive_production": {...}, "baskbear": {...}}
|
|
36
|
-
#
|
|
37
|
-
# When ES_PROFILES is absent, a single profile named "default" is built from
|
|
38
|
-
# the legacy flat ES_*/SSH_* env vars (backward compatible).
|
|
39
40
|
|
|
41
|
+
DEFAULT_PROFILES_FILE = Path.home() / ".es-mcp" / "profiles.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Profile configuration
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
40
47
|
|
|
41
48
|
def _as_bool(value: Any, default: bool = False) -> bool:
|
|
42
49
|
if isinstance(value, bool):
|
|
@@ -90,35 +97,59 @@ def _legacy_profile_from_env() -> dict[str, Any]:
|
|
|
90
97
|
return {key: os.getenv(key.upper()) for key in _SPEC}
|
|
91
98
|
|
|
92
99
|
|
|
100
|
+
def _parse_profiles_data(data: dict) -> tuple[dict[str, dict[str, Any]], str]:
|
|
101
|
+
"""Parse a profiles dict (from env var or file) into (profiles, default)."""
|
|
102
|
+
if isinstance(data.get("profiles"), dict):
|
|
103
|
+
profiles_raw = data["profiles"]
|
|
104
|
+
default = data.get("default", "")
|
|
105
|
+
else:
|
|
106
|
+
profiles_raw = data
|
|
107
|
+
default = ""
|
|
108
|
+
|
|
109
|
+
if not profiles_raw:
|
|
110
|
+
raise ValueError("Profiles config contains no profiles.")
|
|
111
|
+
|
|
112
|
+
profiles = {name: _normalize(cfg) for name, cfg in profiles_raw.items()}
|
|
113
|
+
if not default:
|
|
114
|
+
default = os.getenv("ES_DEFAULT_PROFILE", "").strip() or next(iter(profiles))
|
|
115
|
+
if default not in profiles:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Default profile '{default}' not found. Available: {', '.join(profiles)}"
|
|
118
|
+
)
|
|
119
|
+
return profiles, default
|
|
120
|
+
|
|
121
|
+
|
|
93
122
|
def _load_config() -> tuple[dict[str, dict[str, Any]], str]:
|
|
94
|
-
"""Return (profiles_by_name, default_profile_name).
|
|
123
|
+
"""Return (profiles_by_name, default_profile_name).
|
|
124
|
+
|
|
125
|
+
Priority:
|
|
126
|
+
1. ES_PROFILES env var (JSON string) — backward compat
|
|
127
|
+
2. ES_PROFILES_FILE env var → read that file
|
|
128
|
+
3. ~/.es-mcp/profiles.json auto-discovery
|
|
129
|
+
4. Legacy flat ES_*/SSH_* env vars
|
|
130
|
+
"""
|
|
131
|
+
# 1. ES_PROFILES env var
|
|
95
132
|
raw = os.getenv("ES_PROFILES", "").strip()
|
|
96
133
|
if raw:
|
|
97
134
|
try:
|
|
98
135
|
data = json.loads(raw)
|
|
99
136
|
except json.JSONDecodeError as exc:
|
|
100
137
|
raise ValueError(f"ES_PROFILES is not valid JSON: {exc}") from exc
|
|
138
|
+
return _parse_profiles_data(data)
|
|
139
|
+
|
|
140
|
+
# 2 & 3. File-based config
|
|
141
|
+
file_env = os.getenv("ES_PROFILES_FILE", "").strip()
|
|
142
|
+
profiles_file = Path(os.path.expanduser(file_env)) if file_env else DEFAULT_PROFILES_FILE
|
|
143
|
+
|
|
144
|
+
if profiles_file.exists():
|
|
145
|
+
try:
|
|
146
|
+
with open(profiles_file, encoding="utf-8") as f:
|
|
147
|
+
data = json.load(f)
|
|
148
|
+
return _parse_profiles_data(data)
|
|
149
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
150
|
+
raise ValueError(f"Could not read profiles from {profiles_file}: {exc}") from exc
|
|
101
151
|
|
|
102
|
-
|
|
103
|
-
profiles_raw = data["profiles"]
|
|
104
|
-
default = data.get("default", "")
|
|
105
|
-
else:
|
|
106
|
-
profiles_raw = data
|
|
107
|
-
default = ""
|
|
108
|
-
|
|
109
|
-
if not profiles_raw:
|
|
110
|
-
raise ValueError("ES_PROFILES contains no profiles.")
|
|
111
|
-
|
|
112
|
-
profiles = {name: _normalize(cfg) for name, cfg in profiles_raw.items()}
|
|
113
|
-
if not default:
|
|
114
|
-
default = os.getenv("ES_DEFAULT_PROFILE", "").strip() or next(iter(profiles))
|
|
115
|
-
if default not in profiles:
|
|
116
|
-
raise ValueError(
|
|
117
|
-
f"Default profile '{default}' not found. Available: {', '.join(profiles)}"
|
|
118
|
-
)
|
|
119
|
-
return profiles, default
|
|
120
|
-
|
|
121
|
-
# Legacy single-profile fallback.
|
|
152
|
+
# 4. Legacy single-profile fallback
|
|
122
153
|
return {"default": _normalize(_legacy_profile_from_env())}, "default"
|
|
123
154
|
|
|
124
155
|
|
|
@@ -140,7 +171,6 @@ def _build_es_client(cfg: dict[str, Any], host: str, port: int) -> Elasticsearch
|
|
|
140
171
|
}
|
|
141
172
|
if cfg["es_username"] and cfg["es_password"]:
|
|
142
173
|
kwargs["basic_auth"] = (cfg["es_username"], cfg["es_password"])
|
|
143
|
-
|
|
144
174
|
return Elasticsearch(**kwargs)
|
|
145
175
|
|
|
146
176
|
|
|
@@ -192,6 +222,41 @@ def shutdown():
|
|
|
192
222
|
_tunnels.clear()
|
|
193
223
|
|
|
194
224
|
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Profiles file helpers
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def _get_profiles_file() -> Path:
|
|
230
|
+
"""Return the writable profiles file path (respects ES_PROFILES_FILE env var)."""
|
|
231
|
+
file_env = os.getenv("ES_PROFILES_FILE", "").strip()
|
|
232
|
+
return Path(os.path.expanduser(file_env)) if file_env else DEFAULT_PROFILES_FILE
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _read_profiles_file() -> dict:
|
|
236
|
+
"""Read the profiles file, returning an empty structure if it doesn't exist."""
|
|
237
|
+
f = _get_profiles_file()
|
|
238
|
+
if f.exists():
|
|
239
|
+
with open(f, encoding="utf-8") as fh:
|
|
240
|
+
return json.load(fh)
|
|
241
|
+
return {"default": "", "profiles": {}}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _write_profiles_file(data: dict) -> None:
|
|
245
|
+
"""Write data to the profiles file, creating the directory if needed."""
|
|
246
|
+
f = _get_profiles_file()
|
|
247
|
+
f.parent.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
with open(f, "w", encoding="utf-8") as fh:
|
|
249
|
+
json.dump(data, fh, indent=2)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _reload_profiles() -> None:
|
|
253
|
+
"""Re-read config from disk/env and update the in-memory profile table."""
|
|
254
|
+
global _PROFILES, _DEFAULT_PROFILE
|
|
255
|
+
# Close any cached clients/tunnels for profiles that changed
|
|
256
|
+
shutdown()
|
|
257
|
+
_PROFILES, _DEFAULT_PROFILE = _load_config()
|
|
258
|
+
|
|
259
|
+
|
|
195
260
|
# ---------------------------------------------------------------------------
|
|
196
261
|
# MCP server
|
|
197
262
|
# ---------------------------------------------------------------------------
|
|
@@ -211,6 +276,124 @@ def list_profiles() -> str:
|
|
|
211
276
|
)
|
|
212
277
|
|
|
213
278
|
|
|
279
|
+
@mcp.tool()
|
|
280
|
+
def configure_profile(
|
|
281
|
+
name: str,
|
|
282
|
+
make_default: bool = False,
|
|
283
|
+
es_host: str = "localhost",
|
|
284
|
+
es_port: int = 9200,
|
|
285
|
+
es_username: str = "",
|
|
286
|
+
es_password: str = "",
|
|
287
|
+
es_use_ssl: bool = True,
|
|
288
|
+
es_verify_certs: bool = False,
|
|
289
|
+
ssh_host: str = "",
|
|
290
|
+
ssh_port: int = 22,
|
|
291
|
+
ssh_username: str = "ubuntu",
|
|
292
|
+
ssh_pem_file: str = "~/.ssh/id_rsa",
|
|
293
|
+
ssh_remote_es_host: str = "localhost",
|
|
294
|
+
ssh_remote_es_port: int = 9200,
|
|
295
|
+
ssh_local_port: int = 0,
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Add or update an Elasticsearch profile and save it to ~/.es-mcp/profiles.json.
|
|
298
|
+
|
|
299
|
+
This is the recommended way to configure profiles — no need to edit
|
|
300
|
+
claude_desktop_config.json manually.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
name: Profile name (e.g. "tealive_production", "baskbear").
|
|
304
|
+
make_default: Set this profile as the default.
|
|
305
|
+
es_host: Elasticsearch host (direct connections only; ignored when ssh_host is set).
|
|
306
|
+
es_port: Elasticsearch port.
|
|
307
|
+
es_username: Elasticsearch username (if auth is enabled).
|
|
308
|
+
es_password: Elasticsearch password (if auth is enabled).
|
|
309
|
+
es_use_ssl: True if Elasticsearch uses HTTPS.
|
|
310
|
+
es_verify_certs: True to verify SSL certificates (set False for self-signed).
|
|
311
|
+
ssh_host: Bastion/jump host IP or hostname. Leave empty for a direct connection.
|
|
312
|
+
ssh_port: SSH port on the bastion host.
|
|
313
|
+
ssh_username: SSH login user on the bastion (typically "ubuntu" or "ec2-user").
|
|
314
|
+
ssh_pem_file: Path to the PEM key file on this machine (e.g. ~/keys/prod.pem).
|
|
315
|
+
ssh_remote_es_host: Elasticsearch host as seen from the bastion (usually "localhost").
|
|
316
|
+
ssh_remote_es_port: Elasticsearch port as seen from the bastion.
|
|
317
|
+
ssh_local_port: Local port to bind the tunnel to (0 = auto-pick).
|
|
318
|
+
"""
|
|
319
|
+
if not name.strip():
|
|
320
|
+
return json.dumps({"error": "Profile name cannot be empty."})
|
|
321
|
+
|
|
322
|
+
data = _read_profiles_file()
|
|
323
|
+
|
|
324
|
+
profile_cfg: dict[str, Any] = {
|
|
325
|
+
"es_host": es_host,
|
|
326
|
+
"es_port": es_port,
|
|
327
|
+
"es_use_ssl": es_use_ssl,
|
|
328
|
+
"es_verify_certs": es_verify_certs,
|
|
329
|
+
}
|
|
330
|
+
if es_username:
|
|
331
|
+
profile_cfg["es_username"] = es_username
|
|
332
|
+
if es_password:
|
|
333
|
+
profile_cfg["es_password"] = es_password
|
|
334
|
+
if ssh_host:
|
|
335
|
+
profile_cfg.update({
|
|
336
|
+
"ssh_host": ssh_host,
|
|
337
|
+
"ssh_port": ssh_port,
|
|
338
|
+
"ssh_username": ssh_username,
|
|
339
|
+
"ssh_pem_file": ssh_pem_file,
|
|
340
|
+
"ssh_remote_es_host": ssh_remote_es_host,
|
|
341
|
+
"ssh_remote_es_port": ssh_remote_es_port,
|
|
342
|
+
"ssh_local_port": ssh_local_port,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
if "profiles" not in data or not isinstance(data["profiles"], dict):
|
|
346
|
+
data["profiles"] = {}
|
|
347
|
+
|
|
348
|
+
data["profiles"][name] = profile_cfg
|
|
349
|
+
|
|
350
|
+
if make_default or not data.get("default"):
|
|
351
|
+
data["default"] = name
|
|
352
|
+
|
|
353
|
+
_write_profiles_file(data)
|
|
354
|
+
_reload_profiles()
|
|
355
|
+
|
|
356
|
+
profiles_file = str(_get_profiles_file())
|
|
357
|
+
return json.dumps({
|
|
358
|
+
"ok": True,
|
|
359
|
+
"profile": name,
|
|
360
|
+
"default": _DEFAULT_PROFILE,
|
|
361
|
+
"all_profiles": list(_PROFILES.keys()),
|
|
362
|
+
"saved_to": profiles_file,
|
|
363
|
+
}, indent=2)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
def delete_profile(name: str) -> str:
|
|
368
|
+
"""Remove an Elasticsearch profile from ~/.es-mcp/profiles.json.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
name: Profile name to delete.
|
|
372
|
+
"""
|
|
373
|
+
data = _read_profiles_file()
|
|
374
|
+
profiles = data.get("profiles", {})
|
|
375
|
+
|
|
376
|
+
if name not in profiles:
|
|
377
|
+
return json.dumps({"error": f"Profile '{name}' not found.", "available": list(profiles.keys())})
|
|
378
|
+
|
|
379
|
+
del profiles[name]
|
|
380
|
+
data["profiles"] = profiles
|
|
381
|
+
|
|
382
|
+
# If we deleted the default, pick the next available one
|
|
383
|
+
if data.get("default") == name:
|
|
384
|
+
data["default"] = next(iter(profiles), "")
|
|
385
|
+
|
|
386
|
+
_write_profiles_file(data)
|
|
387
|
+
_reload_profiles()
|
|
388
|
+
|
|
389
|
+
return json.dumps({
|
|
390
|
+
"ok": True,
|
|
391
|
+
"deleted": name,
|
|
392
|
+
"remaining_profiles": list(_PROFILES.keys()),
|
|
393
|
+
"default": _DEFAULT_PROFILE,
|
|
394
|
+
}, indent=2)
|
|
395
|
+
|
|
396
|
+
|
|
214
397
|
@mcp.tool()
|
|
215
398
|
def list_indices(pattern: str = "*", profile: str = "") -> str:
|
|
216
399
|
"""List Elasticsearch indices matching a glob pattern (default: all).
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: artisan-es-reader-plugin
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: Elasticsearch MCP server — query logs with or without SSH tunnel
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: elasticsearch<9.0.0,>=8.0.0
|
|
7
|
-
Requires-Dist: mcp>=1.0.0
|
|
8
|
-
Requires-Dist: paramiko>=3.0.0
|
|
9
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
-
Requires-Dist: sshtunnel>=0.4.0
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
# Elasticsearch MCP Server
|
|
14
|
-
|
|
15
|
-
Query Elasticsearch logs directly from Claude Cowork — with or without an SSH tunnel.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
### 1. Install the plugin
|
|
22
|
-
|
|
23
|
-
Get **es-mcp** from the Cowork plugin marketplace and install it.
|
|
24
|
-
|
|
25
|
-
### 2. Install `uv`
|
|
26
|
-
|
|
27
|
-
The MCP server runs via `uvx`, which requires `uv` to be installed on your machine:
|
|
28
|
-
|
|
29
|
-
```powershell
|
|
30
|
-
# Windows
|
|
31
|
-
powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
# macOS / Linux
|
|
36
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### 3. Configure the MCP server
|
|
40
|
-
|
|
41
|
-
Open `claude_desktop_config.json`:
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
45
|
-
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Add the entry inside `"mcpServers": { }`. Define every Elasticsearch source you
|
|
49
|
-
want to reach as a **profile** under `ES_PROFILES`, and pick which one is used
|
|
50
|
-
when no profile is specified with `"default"`:
|
|
51
|
-
|
|
52
|
-
```json
|
|
53
|
-
{
|
|
54
|
-
"mcpServers": {
|
|
55
|
-
"elasticsearch-logs": {
|
|
56
|
-
"command": "uvx",
|
|
57
|
-
"args": ["artisan-es-reader-plugin@latest", "artisan-es-reader-plugin"],
|
|
58
|
-
"env": {
|
|
59
|
-
"ES_PROFILES": "{\"default\":\"tealive_staging\",\"profiles\":{\"tealive_staging\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"staging-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\staging.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200},\"tealive_production\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"prod-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\prod.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200},\"baskbear\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"baskbear-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\baskbear.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200}}}"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
`ES_PROFILES` is a JSON object (as a string). Because it lives inside JSON, every
|
|
67
|
-
quote is escaped `\"` and Windows backslashes are doubled again to `\\\\`. The
|
|
68
|
-
de-escaped shape is just:
|
|
69
|
-
|
|
70
|
-
```json
|
|
71
|
-
{
|
|
72
|
-
"default": "tealive_staging",
|
|
73
|
-
"profiles": {
|
|
74
|
-
"tealive_staging": { "ssh_host": "...", "ssh_pem_file": "...", "es_username": "...", "es_password": "...", ... },
|
|
75
|
-
"tealive_production": { "ssh_host": "...", "ssh_pem_file": "...", ... },
|
|
76
|
-
"baskbear": { "ssh_host": "...", "ssh_pem_file": "...", ... }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Per-profile keys (all optional; sensible defaults applied):
|
|
82
|
-
|
|
83
|
-
| Key | Description | Default |
|
|
84
|
-
|---|---|---|
|
|
85
|
-
| `es_host` / `es_port` | ES host/port for **direct** connections (ignored when `ssh_host` is set) | `localhost` / `9200` |
|
|
86
|
-
| `es_username` / `es_password` | Elasticsearch credentials | empty |
|
|
87
|
-
| `es_use_ssl` | `true` if ES runs HTTPS | `false` |
|
|
88
|
-
| `es_verify_certs` | `false` for self-signed certs | `false` |
|
|
89
|
-
| `ssh_host` | Bastion / jump host. Leave unset for a direct connection | empty |
|
|
90
|
-
| `ssh_port` / `ssh_username` | SSH port / user | `22` / `ubuntu` |
|
|
91
|
-
| `ssh_pem_file` | Absolute path to your local PEM key (Windows: `C:\\\\Users\\\\you\\\\key.pem`) | `~/.ssh/id_rsa` |
|
|
92
|
-
| `ssh_remote_es_host` / `ssh_remote_es_port` | ES host/port as seen from the bastion | `localhost` / `9200` |
|
|
93
|
-
| `ssh_local_port` | Local tunnel port (`0` = auto) | `0` |
|
|
94
|
-
|
|
95
|
-
### Selecting a profile
|
|
96
|
-
|
|
97
|
-
Every tool takes an optional `profile` argument. Just ask naturally — "search
|
|
98
|
-
**baskbear** logs for X", "recent errors in **tealive production**" — and the
|
|
99
|
-
matching profile is used. Omit it and the `default` profile is queried. Call
|
|
100
|
-
`list_profiles` to see what's configured, or `connection_info` to inspect one.
|
|
101
|
-
|
|
102
|
-
> **Backward compatible:** if `ES_PROFILES` is not set, the server falls back to
|
|
103
|
-
> the old flat `ES_*` / `SSH_*` env vars as a single profile named `default`.
|
|
104
|
-
|
|
105
|
-
### 4. Restart Cowork
|
|
106
|
-
|
|
107
|
-
The SSH tunnel starts automatically on first tool use.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Notes
|
|
112
|
-
|
|
113
|
-
- `es_host` is only used for direct connections. When `ssh_host` is set, traffic routes through the tunnel and `es_host` is ignored.
|
|
114
|
-
- `es_use_ssl`: set `true` if your Elasticsearch runs HTTPS.
|
|
115
|
-
- `es_verify_certs`: set `false` for self-signed certificates.
|
|
116
|
-
- `ssh_local_port`: `0` = auto-pick a free local port.
|
|
117
|
-
- To connect without an SSH tunnel for a profile, leave its `ssh_host` unset.
|
|
118
|
-
- Each profile gets its own client + tunnel, created lazily on first use and reused afterwards.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Updating
|
|
123
|
-
|
|
124
|
-
### For users
|
|
125
|
-
|
|
126
|
-
Updates are automatic — just restart Cowork and `uvx` will pull the latest version from PyPI.
|
|
127
|
-
|
|
128
|
-
### For maintainers
|
|
129
|
-
|
|
130
|
-
1. Make changes to `src/es_mcp/server.py`
|
|
131
|
-
2. Bump the version in `pyproject.toml`
|
|
132
|
-
3. Build and publish:
|
|
133
|
-
```bash
|
|
134
|
-
python -m build
|
|
135
|
-
twine upload dist/*
|
|
136
|
-
```
|
|
137
|
-
4. Users get the new version automatically on next Cowork restart — no action needed on their end
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## Available tools
|
|
142
|
-
|
|
143
|
-
All tools accept an optional `profile` argument to choose the Elasticsearch source.
|
|
144
|
-
|
|
145
|
-
| Tool | What it does |
|
|
146
|
-
|---|---|
|
|
147
|
-
| `list_profiles` | List configured profiles and the default |
|
|
148
|
-
| `list_indices` | List indices (supports glob pattern) |
|
|
149
|
-
| `search_logs` | Full-text search with filters, sort, pagination |
|
|
150
|
-
| `get_recent_errors` | Error-level entries from the last N minutes |
|
|
151
|
-
| `get_index_mapping` | Field schema for an index |
|
|
152
|
-
| `run_aggregation` | Run a custom ES aggregation |
|
|
153
|
-
| `connection_info` | Show active connection / tunnel status |
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Elasticsearch MCP Server
|
|
2
|
-
|
|
3
|
-
Query Elasticsearch logs directly from Claude Cowork — with or without an SSH tunnel.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
### 1. Install the plugin
|
|
10
|
-
|
|
11
|
-
Get **es-mcp** from the Cowork plugin marketplace and install it.
|
|
12
|
-
|
|
13
|
-
### 2. Install `uv`
|
|
14
|
-
|
|
15
|
-
The MCP server runs via `uvx`, which requires `uv` to be installed on your machine:
|
|
16
|
-
|
|
17
|
-
```powershell
|
|
18
|
-
# Windows
|
|
19
|
-
powershell -ExecutionPolicy Bypass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
# macOS / Linux
|
|
24
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 3. Configure the MCP server
|
|
28
|
-
|
|
29
|
-
Open `claude_desktop_config.json`:
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
Windows: %APPDATA%\Claude\claude_desktop_config.json
|
|
33
|
-
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Add the entry inside `"mcpServers": { }`. Define every Elasticsearch source you
|
|
37
|
-
want to reach as a **profile** under `ES_PROFILES`, and pick which one is used
|
|
38
|
-
when no profile is specified with `"default"`:
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"mcpServers": {
|
|
43
|
-
"elasticsearch-logs": {
|
|
44
|
-
"command": "uvx",
|
|
45
|
-
"args": ["artisan-es-reader-plugin@latest", "artisan-es-reader-plugin"],
|
|
46
|
-
"env": {
|
|
47
|
-
"ES_PROFILES": "{\"default\":\"tealive_staging\",\"profiles\":{\"tealive_staging\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"staging-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\staging.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200},\"tealive_production\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"prod-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\prod.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200},\"baskbear\":{\"es_use_ssl\":true,\"es_verify_certs\":false,\"es_username\":\"your-es-username\",\"es_password\":\"your-es-password\",\"ssh_host\":\"baskbear-bastion-ip\",\"ssh_username\":\"ubuntu\",\"ssh_pem_file\":\"C:\\\\Users\\\\your-name\\\\baskbear.pem\",\"ssh_remote_es_host\":\"localhost\",\"ssh_remote_es_port\":9200}}}"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`ES_PROFILES` is a JSON object (as a string). Because it lives inside JSON, every
|
|
55
|
-
quote is escaped `\"` and Windows backslashes are doubled again to `\\\\`. The
|
|
56
|
-
de-escaped shape is just:
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"default": "tealive_staging",
|
|
61
|
-
"profiles": {
|
|
62
|
-
"tealive_staging": { "ssh_host": "...", "ssh_pem_file": "...", "es_username": "...", "es_password": "...", ... },
|
|
63
|
-
"tealive_production": { "ssh_host": "...", "ssh_pem_file": "...", ... },
|
|
64
|
-
"baskbear": { "ssh_host": "...", "ssh_pem_file": "...", ... }
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Per-profile keys (all optional; sensible defaults applied):
|
|
70
|
-
|
|
71
|
-
| Key | Description | Default |
|
|
72
|
-
|---|---|---|
|
|
73
|
-
| `es_host` / `es_port` | ES host/port for **direct** connections (ignored when `ssh_host` is set) | `localhost` / `9200` |
|
|
74
|
-
| `es_username` / `es_password` | Elasticsearch credentials | empty |
|
|
75
|
-
| `es_use_ssl` | `true` if ES runs HTTPS | `false` |
|
|
76
|
-
| `es_verify_certs` | `false` for self-signed certs | `false` |
|
|
77
|
-
| `ssh_host` | Bastion / jump host. Leave unset for a direct connection | empty |
|
|
78
|
-
| `ssh_port` / `ssh_username` | SSH port / user | `22` / `ubuntu` |
|
|
79
|
-
| `ssh_pem_file` | Absolute path to your local PEM key (Windows: `C:\\\\Users\\\\you\\\\key.pem`) | `~/.ssh/id_rsa` |
|
|
80
|
-
| `ssh_remote_es_host` / `ssh_remote_es_port` | ES host/port as seen from the bastion | `localhost` / `9200` |
|
|
81
|
-
| `ssh_local_port` | Local tunnel port (`0` = auto) | `0` |
|
|
82
|
-
|
|
83
|
-
### Selecting a profile
|
|
84
|
-
|
|
85
|
-
Every tool takes an optional `profile` argument. Just ask naturally — "search
|
|
86
|
-
**baskbear** logs for X", "recent errors in **tealive production**" — and the
|
|
87
|
-
matching profile is used. Omit it and the `default` profile is queried. Call
|
|
88
|
-
`list_profiles` to see what's configured, or `connection_info` to inspect one.
|
|
89
|
-
|
|
90
|
-
> **Backward compatible:** if `ES_PROFILES` is not set, the server falls back to
|
|
91
|
-
> the old flat `ES_*` / `SSH_*` env vars as a single profile named `default`.
|
|
92
|
-
|
|
93
|
-
### 4. Restart Cowork
|
|
94
|
-
|
|
95
|
-
The SSH tunnel starts automatically on first tool use.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Notes
|
|
100
|
-
|
|
101
|
-
- `es_host` is only used for direct connections. When `ssh_host` is set, traffic routes through the tunnel and `es_host` is ignored.
|
|
102
|
-
- `es_use_ssl`: set `true` if your Elasticsearch runs HTTPS.
|
|
103
|
-
- `es_verify_certs`: set `false` for self-signed certificates.
|
|
104
|
-
- `ssh_local_port`: `0` = auto-pick a free local port.
|
|
105
|
-
- To connect without an SSH tunnel for a profile, leave its `ssh_host` unset.
|
|
106
|
-
- Each profile gets its own client + tunnel, created lazily on first use and reused afterwards.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Updating
|
|
111
|
-
|
|
112
|
-
### For users
|
|
113
|
-
|
|
114
|
-
Updates are automatic — just restart Cowork and `uvx` will pull the latest version from PyPI.
|
|
115
|
-
|
|
116
|
-
### For maintainers
|
|
117
|
-
|
|
118
|
-
1. Make changes to `src/es_mcp/server.py`
|
|
119
|
-
2. Bump the version in `pyproject.toml`
|
|
120
|
-
3. Build and publish:
|
|
121
|
-
```bash
|
|
122
|
-
python -m build
|
|
123
|
-
twine upload dist/*
|
|
124
|
-
```
|
|
125
|
-
4. Users get the new version automatically on next Cowork restart — no action needed on their end
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Available tools
|
|
130
|
-
|
|
131
|
-
All tools accept an optional `profile` argument to choose the Elasticsearch source.
|
|
132
|
-
|
|
133
|
-
| Tool | What it does |
|
|
134
|
-
|---|---|
|
|
135
|
-
| `list_profiles` | List configured profiles and the default |
|
|
136
|
-
| `list_indices` | List indices (supports glob pattern) |
|
|
137
|
-
| `search_logs` | Full-text search with filters, sort, pagination |
|
|
138
|
-
| `get_recent_errors` | Error-level entries from the last N minutes |
|
|
139
|
-
| `get_index_mapping` | Field schema for an index |
|
|
140
|
-
| `run_aggregation` | Run a custom ES aggregation |
|
|
141
|
-
| `connection_info` | Show active connection / tunnel status |
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"mcpServers": {
|
|
3
|
-
"elasticsearch-logs": {
|
|
4
|
-
"command": "uvx",
|
|
5
|
-
"args": ["--from", "git+https://github.com/artisan-soohao/es-mcp.git", "es-mcp"],
|
|
6
|
-
"env": {
|
|
7
|
-
"ES_HOST": "localhost",
|
|
8
|
-
"ES_PORT": "9200",
|
|
9
|
-
"ES_USERNAME": "",
|
|
10
|
-
"ES_PASSWORD": "",
|
|
11
|
-
"ES_USE_SSL": "false",
|
|
12
|
-
"ES_VERIFY_CERTS": "false",
|
|
13
|
-
|
|
14
|
-
"SSH_HOST": "",
|
|
15
|
-
"SSH_PORT": "22",
|
|
16
|
-
"SSH_USERNAME": "ubuntu",
|
|
17
|
-
"SSH_PEM_FILE": "C:/Users/NgSooHao/.ssh/my-key.pem",
|
|
18
|
-
"SSH_REMOTE_ES_HOST": "localhost",
|
|
19
|
-
"SSH_REMOTE_ES_PORT": "9200",
|
|
20
|
-
"SSH_LOCAL_PORT": "0"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
File without changes
|
|
File without changes
|
{artisan_es_reader_plugin-0.3.0 → artisan_es_reader_plugin-0.4.0}/artisan-es-reader-plugin.plugin
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|