Skip to main content
Glama
KietDev-JS

o2-readonly

by KietDev-JS
README.md
# mcp-openobserve-readonly

A read-only [MCP](https://modelcontextprotocol.io) server for [OpenObserve](https://openobserve.ai).
It lets an AI assistant list your log streams, inspect their fields and run `SELECT` queries against
them — and nothing else.

One file, no dependencies, stdio transport. Node 18 or newer.

```
you ──▶ assistant ──▶ o2-readonly (this server) ──▶ OpenObserve HTTP API
                        │
                        └── only 3 endpoints, SELECT-only SQL, capped rows and time window
```

## Why it exists

Handing an assistant a generic HTTP tool and your OpenObserve credential means it can call any
endpoint the credential allows, including ones that change things. This server sits in between and
refuses everything except three read paths, so an assistant investigating an incident can explore
logs freely without you worrying about what else it might reach.

## Tools

| Tool | What it does |
|---|---|
| `o2_list_streams` | Lists streams with type, document count and last event time. Filter by name substring or stream type; up to 500 results. |
| `o2_stream_schema` | Field names and types for one stream. |
| `o2_search` | Runs a single `SELECT` / `WITH` query against a stream. |

### `o2_search` parameters

| Name | Default | Notes |
|---|---|---|
| `sql` | required | One `SELECT` or `WITH` statement. Quote the stream name: `SELECT * FROM "my_stream"`. |
| `minutes` | `15` | How far back to look from `end`. |
| `start` / `end` | `end` = now | ISO-8601. `start` overrides `minutes`. |
| `size` | `50` | Rows to return, maximum 200. |
| `type` | `logs` | `logs`, `metrics` or `traces`. |

```jsonc
// list the streams whose name contains "api"
{ "name": "o2_list_streams", "arguments": { "filter": "api" } }

// errors in the last hour
{ "name": "o2_search", "arguments": {
    "sql": "SELECT _timestamp, level, message FROM \"my_stream\" WHERE level = 'error' ORDER BY _timestamp DESC",
    "minutes": 60, "size": 100 } }

// a specific window, hours grouped
{ "name": "o2_search", "arguments": {
    "sql": "SELECT histogram(_timestamp, '1 hour') AS hr, count(*) AS n FROM \"my_stream\" GROUP BY hr ORDER BY hr",
    "start": "2026-03-26T00:00:00Z", "end": "2026-03-27T00:00:00Z" } }
```

## What it refuses

Enforced inside the process, whatever the credential is allowed to do:

- **Three endpoints only** — `GET /api/{org}/streams`, `GET /api/{org}/streams/{stream}/schema`,
  `POST /api/{org}/_search`. The only `POST` is the search itself. Paths that change under URL
  normalisation are rejected.
- **`SELECT` / `WITH` only** — one statement, no semicolons, no SQL comments, unbalanced quotes
  rejected, and a keyword deny-list (`insert`, `update`, `delete`, `drop`, `alter`, `create`,
  `truncate`, `grant`, `revoke`, `merge`, `copy`, `attach`, `detach`, `exec`, `execute`) outside
  string literals. The search API can't write anyway; this is a second line of defence.
- **Caps** — 200 rows, 500 characters per cell, 60,000 characters of output, and a time window of
  `O2_MAX_WINDOW_MIN` minutes (1440 by default). Oversized results are halved until they fit and
  marked `truncated`.
- Stream names must match `[A-Za-z0-9_][A-Za-z0-9_.-]*`, and the organisation name `[A-Za-z0-9_-]+`.

> **It does not weaken the credential itself.** These limits live in this process. If someone runs a
> different client with the same credential, they get whatever that credential allows. For a real
> guarantee, put a **non-admin, read-only OpenObserve user** in `O2_AUTH` — see
> [SETUP.md](SETUP.md).

## Configuration

| Variable | Default | Meaning |
|---|---|---|
| `O2_AUTH` | — | **Required.** `Basic <base64 of email:password>`. On Windows, falls back to the user-level environment variable read from the registry. |
| `O2_BASE_URL` | `http://localhost:5080` | Your OpenObserve base URL. |
| `O2_ORG` | `default` | Organisation name. |
| `O2_MAX_WINDOW_MIN` | `1440` | Largest allowed query window, in minutes. |

## Quick start

```bash
git clone https://github.com/KietDev-JS/mcp-openobserve-readonly.git
cd mcp-openobserve-readonly

# Claude Code
claude mcp add o2-readonly \
  -e O2_BASE_URL="https://openobserve.example.com" \
  -e O2_AUTH="Basic $(printf 'you@example.com:password' | base64)" \
  -- node "$PWD/server.mjs"

claude mcp list   # expect: o2-readonly … ✔ Connected
```

Full walkthrough, including Claude Desktop and other MCP clients, credential setup and
troubleshooting: **[SETUP.md](SETUP.md)**.

## Notes and limits

- **A query window covers at most 24 hours** by default. To look further back, query one day at a
  time with explicit `start` and `end`, or raise `O2_MAX_WINDOW_MIN`.
- **`histogram(_timestamp, '1 hour')`** is the practical way to see the shape of a log volume over
  time.
- **Timestamps are microseconds.** Results add a readable `_time` field alongside `_timestamp`.
- The server speaks MCP protocol versions `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`,
  and echoes back whichever the client asks for if it recognises it.

## License

[MIT](LICENSE).