OpenSearch MCP Server
by rajanmishra6
README.md
# OpenSearch MCP Server
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that lets AI assistants such as Claude,
ChatGPT and Cursor search and inspect an OpenSearch cluster.
It runs in two modes:
* **Local (stdio):** the AI app starts the server as a child process on your machine. No network exposure and no
token needed.
* **Remote (Streamable HTTP):** one shared server (production: `https://opensearch-mcp.monotype.com`) that clients
reach over HTTPS. Every request must carry a valid API token or an OAuth token derived from one.
## Tools
| Area | Tool | What it does |
| --- | --- | --- |
| Indices | `list_indices` | List all indices |
| | `get_mapping` | Mapping of an index |
| | `get_settings` | Settings of an index |
| Documents | `search_documents` | Search an index with Query DSL (default size 20) |
| Cluster | `get_cluster_health` | Cluster health |
| | `get_cluster_stats` | Cluster-wide statistics |
| Dashboards | `list_index_patterns` | Index patterns stored in `.kibana` |
| | `generate_discover_url` | Build an OpenSearch Dashboards Discover link for a query |
| Admin | `get_ism_policies` | Index State Management policies |
| | `get_index_templates` | Index templates and their shard counts |
| | `get_shard_allocation` | Shard distribution across nodes |
| | `get_hot_threads` | Hot threads on all nodes |
| | `get_tasks` | Running cluster tasks |
| | `get_recovery_status` | Shard recovery progress and ETA |
All tools are read-only.
## Repository layout
This is the **code repository**. Deployment lives in a separate **build repository** (Helm chart, image
Dockerfile, Jenkins pipeline). Jenkins checks out both and builds the image from this repository's `src/`.
```
src/opensearch_mcp_server/
server.py entry point (stdio, or Streamable HTTP with --transport streamable-http)
es_client.py OpenSearch client (reads OPENSEARCH_* variables)
tools/ the MCP tools listed above
auth/
store.py SQLite store for API tokens and OAuth grants (hashes only)
provider.py OAuth 2.1 authorization server backed by API tokens
http_app.py authenticated Streamable HTTP app (discovery, sign-in page, /mcp)
cli.py opensearch-mcp-token command
tests/ pytest suite, including a fake OpenSearch server
```
The build repository's image replaces `server.py` with its own `k8s/api_image/app/server.py` and installs with its
own `pyproject.toml` and `constraints.txt`. That version uses the official MCP SDK's `FastMCP`, serves the endpoint
under a secret `MCP_PATH_TOKEN` prefix and adds a Host-header check. It still uses the `tools/` and `auth/` packages
from this repository. When you change tools or authentication here, the next pipeline run picks them up. If you add
a dependency, add it to the build repository's `app/pyproject.toml` and `constraints.txt` too.
## Requirements
* Python 3.10 or newer (the production image uses 3.11)
* [uv](https://docs.astral.sh/uv/) for local development
* Network access to an OpenSearch cluster and credentials for it
```bash
git clone <code-repo-url> opensearch-mcp-server
cd opensearch-mcp-server
uv sync
```
## Configuration
Settings come from environment variables. For local work, copy `.env-template` to `.env` (git-ignored); the server
loads it automatically.
| Variable | Default | Purpose |
| --- | --- | --- |
| `OPENSEARCH_HOST` | | OpenSearch URL, e.g. `https://localhost:9200` |
| `OPENSEARCH_USERNAME`, `OPENSEARCH_PASSWORD` | | OpenSearch credentials (required) |
| `DASHBOARDS_HOST` | | OpenSearch Dashboards URL, used by `generate_discover_url` |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http` (the `--transport` flag overrides it) |
| `MCP_HOST` / `MCP_PORT` | `127.0.0.1` / `8000` | Listen address for HTTP mode |
| `MCP_PUBLIC_URL` | `http://<host>:<port>` | Public origin, no path, e.g. `https://opensearch-mcp.monotype.com`. OAuth metadata, the resource URL and the `401` discovery header are built from it, never from request headers. Must be HTTPS except on localhost |
| `MCP_HTTP_PATH` | `/mcp` | Path of the MCP endpoint (the production image derives it from `MCP_PATH_TOKEN` instead) |
| `MCP_AUTH_DB` | `~/.opensearch-mcp/auth.db` | Token database. The server and `opensearch-mcp-token` must use the same file |
| `MCP_ALLOWED_HOSTS` | | Comma-separated Host header values accepted on the MCP endpoint (DNS-rebinding protection) |
| `MCP_FORWARDED_ALLOW_IPS` | `127.0.0.1` | Proxies trusted for `X-Forwarded-*` (only affects logged client IPs) |
| `MCP_OAUTH_ACCESS_TOKEN_TTL` | `3600` | OAuth access token lifetime, seconds |
| `MCP_OAUTH_REFRESH_TOKEN_TTL` | `2592000` | OAuth refresh token lifetime, seconds (30 days) |
| `MCP_OAUTH_REDIRECT_ALLOWLIST` | Claude, ChatGPT, Cursor, loopback | Comma-separated redirect URIs clients may register; a trailing `*` is a prefix match, `*` alone allows any |
| `MCP_STATELESS` | `false` | Stateless Streamable HTTP (for several replicas without sticky sessions) |
| `MCP_JSON_RESPONSE` | `false` | Answer POSTs with plain JSON instead of an SSE stream |
## Local use with Claude Desktop (stdio)
Add this to `claude_desktop_config.json`, with `--directory` pointing at your clone:
```json
{
"mcpServers": {
"opensearch": {
"command": "uv",
"args": ["--directory", "/path/to/opensearch-mcp-server", "run", "opensearch-mcp-server"],
"env": {
"OPENSEARCH_HOST": "https://localhost:9200",
"OPENSEARCH_USERNAME": "",
"OPENSEARCH_PASSWORD": "",
"DASHBOARDS_HOST": "https://localhost:5601"
}
}
}
}
```
The file is at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and
`%APPDATA%\Claude\claude_desktop_config.json` on Windows. Restart Claude Desktop, then ask things like
"List all indices in the cluster" or "Show me the cluster health status".
## Running the remote (HTTP) server locally
```bash
uv run opensearch-mcp-token create --name local-dev --expires 1d # prints the token once
uv run opensearch-mcp-server --transport streamable-http # http://127.0.0.1:8000/mcp
```
Then check it:
```bash
curl -i -X POST http://127.0.0.1:8000/mcp # 401 with a WWW-Authenticate header
curl -i -X POST http://127.0.0.1:8000/mcp \
-H "Authorization: Bearer osmcp_..." \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```
[MCP Inspector](https://github.com/modelcontextprotocol/inspector) (`npx @modelcontextprotocol/inspector`) also
works. Choose Streamable HTTP and add the `Authorization` header.
## Authentication
### How it works
* **API tokens** are created by an operator with `opensearch-mcp-token`. They look like `osmcp_<id>_<secret>`.
Clients that can send a fixed header use `Authorization: Bearer osmcp_...` directly.
* **OAuth** is for clients that only support MCP OAuth, such as Claude custom connectors and ChatGPT. The client
discovers this server's authorization server from the `401` response, registers itself (Dynamic Client
Registration) and opens a sign-in page in the browser. There the user pastes an API token. The client then gets
short-lived access tokens and rotating refresh tokens. These are tied to that API token: they never outlive it,
and they stop working the moment it expires, is revoked or is rotated.
* Requests without a valid token get `401` and never reach OpenSearch. Tool calls re-check the token right before
running. A session opened with one token cannot be used with another.
* Only SHA-256 hashes of tokens, authorization codes and client secrets are stored, in a SQLite file created with
mode `0600`. Tokens and the secret URL prefix are redacted from logs.
### Managing tokens
```bash
opensearch-mcp-token create --name <label> # asks for an expiration
opensearch-mcp-token list [--all] # id, name, status, created, expires, last used
opensearch-mcp-token rotate <id> [--grace 1h] # new token (asks for its expiration); old one revoked
opensearch-mcp-token revoke <id> # immediate, including OAuth sessions created with it
```
Every `create` and `rotate` asks **"When should this token expire?"**. Accepted answers:
* a date, `2026-12-31`, meaning the end of that day in UTC
* a date and time, `2026-12-31T18:00` (UTC) or `2026-12-31T18:00+05:30`
* a duration from now: `30m`, `12h`, `90d`, `8w`
* `none` for a token that never expires (you are asked to confirm)
Past or unreadable values are asked again. `--expires <value>` skips the prompt for scripts. The token is printed
once on stdout (other messages go to stderr), so `TOKEN=$(opensearch-mcp-token create --name ci --expires 30d)`
works. It cannot be shown again; create a new one if it is lost.
Changes apply on the next request, with no restart. Use one token per person or integration so you can revoke
them separately. With `rotate --grace 1h`, the old token keeps working for an hour so clients can switch over.
## Connecting clients
Use the endpoint URL **`https://opensearch-mcp.monotype.com/<MCP_PATH_TOKEN>/mcp`** in production, or
`http://127.0.0.1:8000/mcp` locally. Ask the operator for the URL and an API token.
### Claude (claude.ai, Desktop, mobile)
Claude custom connectors use OAuth by default. Sending a fixed `Authorization` header is a
[beta that only some organizations have](https://claude.com/docs/connectors/building/authentication).
**OAuth (works on every plan):**
1. Open **Settings → Connectors → Add custom connector**. On Team/Enterprise, an Owner first adds it under
**Organization settings → Connectors → Add → Custom → Web**.
2. Enter a name and the endpoint URL. Leave the OAuth client ID and secret empty.
3. Click **Connect**. A browser page from this server opens.
4. Paste your API token and click **Authorize**. Claude lists the 14 tools.
When the API token expires or is revoked, Claude asks you to connect again. Paste a new token.
**Request headers (beta organizations only):** when adding the connector, choose no sign-in. Add the header
`Authorization` with the value `Bearer osmcp_...`. Type the `Bearer ` prefix yourself, because Claude sends the
value exactly as entered.
### Claude Code
```bash
claude mcp add --transport http opensearch https://opensearch-mcp.monotype.com/<MCP_PATH_TOKEN>/mcp \
--header "Authorization: Bearer osmcp_..."
```
Or leave out `--header` and run `/mcp` in Claude Code to sign in through the browser.
### Claude API (MCP connector)
Pass the endpoint URL as the MCP server `url` and the API token as `authorization_token`.
### ChatGPT
Enable developer mode, add a custom MCP server with the endpoint URL and choose OAuth. Leave the client fields
empty; ChatGPT registers itself, and you sign in with your API token.
### Cursor, VS Code and other clients
Any client that can set headers works with the token directly, e.g. in Cursor's `mcp.json`:
```json
{
"mcpServers": {
"opensearch": {
"url": "https://opensearch-mcp.monotype.com/<MCP_PATH_TOKEN>/mcp",
"headers": { "Authorization": "Bearer osmcp_..." }
}
}
}
```
## Production deployment
Production runs on EKS behind an internet-facing AWS NLB (TLS terminated with ACM) and Cloudflare. Everything for
it is in the build repository.
| Build repo path | Purpose |
| --- | --- |
| `k8s/api_image/` | Image `Dockerfile` (Python 3.11, non-root, read-only root filesystem), `app/server.py`, `app/pyproject.toml`, `constraints.txt`, `prebuildsteps.sh` (build and push to ECR) |
| `k8s/helm/` | Chart: Deployment, Service, NLB, ConfigMap, Secret, PersistentVolumeClaim for the token database |
| `k8s/helm/prod/` | `prod.yaml` values and sops-encrypted `secrets.prod.yaml` (`OPENSEARCH_PASSWORD`, `MCP_PATH_TOKEN`) |
| `Makefile` | `build_api_image`, `deploy_image_to_prod_k8`, `lint`, `lb_info`, `token_*` targets |
| `push_to_env/prod.groovy` | Jenkins pipeline: check out both repos, build, deploy with `helm secrets`, record the version |
### Pipeline
Jenkins checks out this repository (the "Codeget" stage) and the build repository. `prebuildsteps.sh` moves
`k8s/api_image/*` to the workspace root and builds and pushes `<ECR>/opensearch-mcp:<build>`. Then
`make deploy_image_to_prod_k8` runs `helm secrets upgrade --install --atomic`. A failed rollout is rolled back
automatically.
**Before the first run:** the "Codeget" stage in `push_to_env/prod.groovy` must point at this code repository's
URL and branch.
### What the chart sets up
* `MCP_TRANSPORT=streamable-http`. `MCP_PUBLIC_URL` and `MCP_ALLOWED_HOSTS` come from `INGRESS_HOST`
(`opensearch-mcp.monotype.com`).
* `MCP_PATH_TOKEN` (sops secret) is a secret URL prefix on top of token authentication. It is not a replacement
for it.
* A 1Gi ReadWriteOnce volume, `opensearch-mcp-data`, is mounted at `/data` with `MCP_AUTH_DB=/data/auth.db`. It is
annotated `helm.sh/resource-policy: keep`, so uninstalling the release does not delete the tokens. It uses the
cluster's default StorageClass; set `persistence.storageClassName` (e.g. `gp3`) in `prod.yaml` if there is none.
Check with `kubectl get storageclass`. To reuse a restored volume, set `persistence.existingClaim`.
* Strategy `Recreate`: only one pod can mount the volume, so the old pod stops before the new one starts. Expect
about a minute of downtime per deploy. The chart refuses to render with a rolling update while persistence is on.
* One replica. Sessions are held in memory and the volume is single-attach.
* The NLB, the kubelet probes and `helm test` use `/health`, which needs no token.
### First deploy and token management
After the first deploy there are no tokens, so every MCP request is rejected. From the build repository, with
the production kube context selected:
```bash
make token_create NAME=rajan-claude # interactive: asks for the expiration
make token_create NAME=ci-bot EXPIRES=90d # non-interactive
make token_list [ALL=1]
make token_rotate ID=<id> [GRACE=1h] [EXPIRES=...]
make token_revoke ID=<id>
```
These run `opensearch-mcp-token` inside the running pod
(`kubectl -n opensearch exec -it deploy/opensearch-mcp -- opensearch-mcp-token ...`).
### Network and proxy requirements
* All of these paths must reach the pod: `/<MCP_PATH_TOKEN>/mcp`,
`/.well-known/oauth-protected-resource/<MCP_PATH_TOKEN>/mcp`, `/.well-known/oauth-authorization-server`,
`/authorize`, `/token`, `/register`, `/revoke`, `/oauth/login`, `/health`. The NLB forwards everything.
* Cloudflare: Bot Fight Mode, WAF challenges or Cloudflare Access on these paths will break Claude and ChatGPT.
Claude's traffic comes from `160.79.104.0/21`. Rate limiting on `/oauth/login`, `/register` and `/token` is
recommended.
* The `Authorization` header must be passed through. Responses must not be buffered, and OAuth endpoints must
answer within Claude's 10 second limit.
### Backups
The token database is the only state. Back it up with
`kubectl -n opensearch exec deploy/opensearch-mcp -- python -c "import sqlite3; s=sqlite3.connect('/data/auth.db'); d=sqlite3.connect('/tmp/backup.db'); s.backup(d)"`
followed by `kubectl cp`, or with EBS snapshots. It contains only hashes; if it is lost, issue new tokens.
## Testing
```bash
uv sync
uv run pytest
```
The suite starts the real server against a fake OpenSearch (`tests/fake_opensearch.py`), so no cluster is needed.
It covers:
* MCP initialization, tool discovery and tool calls
* valid, invalid, expired, non-expiring, revoked and rotated tokens, including revocation in the middle of a session
* the full Claude OAuth flow, from the `401` to discovery, registration, PKCE sign-in, code exchange, refresh
rotation and revocation
* the stdio transport
* the token CLI prompts
`tests/test_deploy_image.py` covers the production image: secret prefix, Host-header check and log redaction. It
is skipped unless the installed package is the image build. To run it, copy `src/` plus the build repository's
`app/server.py`, `app/pyproject.toml` and `constraints.txt` into a directory as the Dockerfile does,
`pip install --constraint constraints.txt .` into a Python 3.11 virtualenv, then run `python -m pytest` from this
repository with that interpreter.
## Security notes and limitations
* The sign-in page asks for the API token itself. Users should only paste it into a sign-in they started from their
own AI client. The page shows where it will redirect, and redirect URIs are limited by
`MCP_OAUTH_REDIRECT_ALLOWLIST`.
* Everyone who uses the same token shares one identity, and every token can use every tool. There are no per-tool
scopes.
* OpenSearch is accessed with a single service account (`OPENSEARCH_USERNAME`), whatever the MCP caller. The client
is created with certificate verification disabled (`verify_certs=False`).
* Client ID Metadata Documents are not supported, so Claude and ChatGPT use Dynamic Client Registration, which
creates one client record per connection.
* Authentication covers Streamable HTTP only. The old SSE transport is not available in HTTP mode. stdio stays
local and unauthenticated by design.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues