openobserve-readonly-mcp
by KietDev-JS
README.md
# openobserve-readonly-mcp
[](https://github.com/KietDev-JS/openobserve-readonly-mcp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/openobserve-readonly-mcp)
[](https://nodejs.org)
[](LICENSE)
A read-only [MCP](https://modelcontextprotocol.io) server for
[OpenObserve](https://openobserve.ai). It lets an AI assistant list streams,
inspect schemas, and run bounded SQL searches over your logs, metrics and
traces — without the ability to write anything.
Zero runtime dependencies; it uses only the Node standard library.
## Why read-only matters here
Giving a model log search is useful. Giving it an observability API token is a
different proposition: the same credential that reads logs can often delete
streams, change retention, or edit alerts.
This server narrows that surface in-process:
- **Three endpoints, allowlisted.** Every outbound request is matched against
an allowlist before it is sent. The only `POST` is `/_search`. Anything else
— including a request the model constructs itself — is refused locally.
- **SELECT/WITH only.** SQL is parsed for structure before it leaves the
process: no stacked statements, no comments, no DDL/DML keywords outside
string literals.
- **Bounded output.** Row counts, string sizes, the time window, and the total
response size are all capped, so one careless query cannot flood the context
window or the upstream server.
**This does not reduce the privileges of the credential you supply.** The
allowlist constrains this process, not your token. For an actual guarantee,
create a read-only OpenObserve user and put *that* in `O2_AUTH`. The two
layers are complementary: the token bounds what is possible, this server
bounds what is attempted.
## Tools
| Tool | Purpose |
| --- | --- |
| `o2_list_streams` | List streams with type, document count, last event time. Supports substring and type filters. |
| `o2_stream_schema` | Field names and types for one stream. Use it before writing a query. |
| `o2_search` | Run one `SELECT`/`WITH` query over a bounded time window. |
All three are annotated `readOnlyHint: true`.
## Install
Requires Node.js 18.17 or newer.
```bash
npx openobserve-readonly-mcp
```
Or install globally:
```bash
npm install -g openobserve-readonly-mcp
```
Or from source:
```bash
git clone https://github.com/KietDev-JS/openobserve-readonly-mcp.git
cd openobserve-readonly-mcp
npm test
```
## Configuration
Configuration is environment-only. There is no default host: the server exits
with status 2 and an explanatory message rather than guessing where to send
your credentials.
| Variable | Required | Default | Notes |
| --- | --- | --- | --- |
| `O2_BASE_URL` | yes | — | e.g. `https://openobserve.example.com`. A reverse-proxy subpath such as `https://host/observe` is supported. |
| `O2_AUTH` | yes | — | `Basic <base64 of email:password>` or `Bearer <token>`. |
| `O2_ORG` | no | `default` | OpenObserve organization identifier. |
| `O2_MAX_WINDOW_MIN` | no | `1440` | Largest allowed search window, in minutes. |
| `O2_TIMEOUT_MS` | no | `60000` | Upstream request timeout. |
| `O2_DEBUG` | no | — | Log the resolved config (credential redacted) to stderr. |
Generate the Basic value with:
```bash
printf 'you@example.com:YOUR_PASSWORD' | base64
```
## Client setup
Claude Desktop (`claude_desktop_config.json`), Claude Code (`~/.claude.json`):
```json
{
"mcpServers": {
"openobserve": {
"command": "npx",
"args": ["-y", "openobserve-readonly-mcp"],
"env": {
"O2_BASE_URL": "https://openobserve.example.com",
"O2_AUTH": "Basic BASE64_HERE"
}
}
}
}
```
opencode (`opencode.json`):
```json
{
"mcp": {
"openobserve": {
"type": "local",
"command": ["npx", "-y", "openobserve-readonly-mcp"],
"enabled": true,
"environment": {
"O2_BASE_URL": "https://openobserve.example.com",
"O2_AUTH": "Basic BASE64_HERE"
}
}
}
}
```
See [docs/SETUP.md](docs/SETUP.md) for a step-by-step walkthrough, including a
connectivity check to run *before* touching any MCP config.
## Querying
Stream names must be double-quoted, and results are ordered by `_timestamp`
descending in practice:
```sql
SELECT _timestamp, level, message
FROM "app_logs"
WHERE level = 'ERROR'
ORDER BY _timestamp DESC
```
Useful details:
- `_timestamp` is **microseconds** since the epoch. Each returned row also
carries an ISO-8601 `_time` field for readability.
- The window defaults to the last 15 minutes. Pass `minutes`, or an explicit
`start`/`end` ISO-8601 pair.
- `size` defaults to 50 and is capped at 200 rows.
- Keywords inside string literals are fine: searching for the text
`'delete'` is not mistaken for a `DELETE` statement.
## Limits
| Limit | Value | Why |
| --- | --- | --- |
| Rows per search | 200 | Keeps a single call from flooding the context window. |
| Streams per listing | 500 | Same. |
| String cell length | 500 chars | Clipped at any nesting depth; long stack traces are the usual culprit. |
| Response size | 60 KB | Rows are shed progressively; output is always valid JSON. |
| Time window | 1440 min | Bounds upstream query cost. Raise with `O2_MAX_WINDOW_MIN`. |
When a result is reduced, the response includes a `truncated` object recording
how many rows were returned out of how many matched — the reduction is
reported rather than silent.
## Security notes
- The credential is never echoed in error messages or logs; `O2_DEBUG` output
redacts it.
- Stream names are validated against `^[A-Za-z0-9_][A-Za-z0-9_.-]*$`, and any
request whose path changes under URL normalization is refused, so traversal
attempts cannot escape the allowlisted routes.
- Prefer passing `O2_AUTH` through your MCP client's `env` block over exporting
it into your shell profile.
## Development
```bash
npm test # 141 tests, no network access required
npm run test:coverage # ~98% line coverage
```
The suite injects a fake `fetch`, so it runs fully offline and deterministically.
## License
MIT
TDQS
A4.4/5.0
Scored across 3 tools
Disambiguation5/5
The three tools serve clearly distinct purposes: listing streams, retrieving schema, and running queries. No overlap exists, so an agent can confidently select the right tool.
Naming Consistency4/5
All tools share the 'o2_' prefix, and most follow a verb_noun pattern (list_streams, search). 'stream_schema' is a slight deviation as it's noun_phrase, but it remains predictable and readable.
Tool Count5/5
With only 3 tools, the server is tightly scoped for read-only access to OpenObserve. Each tool is essential and earns its place, making the set easy to navigate.
Completeness5/5
For a read-only server, the surface is complete: discover streams, inspect schemas, and execute queries. These cover the core workflows without missing critical operations.