Skip to content

Commit dc3a7b7

Browse files
authored
feat: migrated tools to Parseable registry (#23)
1 parent 56b6289 commit dc3a7b7

54 files changed

Lines changed: 683 additions & 2281 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ PARSEABLE_URL=http://localhost:8000
33
PARSEABLE_API_KEY=your-api-key
44

55
# Optional tool defaults
6-
PARSEABLE_DEFAULT_DATASET=
7-
PARSEABLE_MAX_ROWS=1000
86
PARSEABLE_QUERY_TIMEOUT_MS=30000
97

108
# HTTP mode (used when running: node dist/server.js http)

‎CONTRIBUTING.md‎

Lines changed: 44 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,61 @@
11
# Contributing
22

3-
Thanks for helping improve `parseable-mcp-server`. This document covers the design principles, layout, and the practical mechanics of adding a tool or fixing a bug.
4-
5-
## Design principles
6-
7-
1. **Auth is the trust boundary.** If a user supplies credentials, they trust the MCP client. No env-var write-gates - MCP clients already show per-call approval UI. Re-introduce gates only for hosted/multi-tenant variants.
8-
2. **Time-bounded queries are mandatory.** Never unbounded scans. Default window 60 minutes, max 1440.
9-
3. **Row caps are enforced.** Default 100, hard max via `PARSEABLE_MAX_ROWS` (default 1000).
10-
4. **One tool, one verb.** No mega-tools.
11-
5. **Tool descriptions are user-facing.** They tell the calling client when to use the tool, not just what.
12-
6. **No `ingest_event` tool.** Wrong shape, encourages garbage data - use Parseable's ingest API directly.
13-
7. **No destructive tools** (`delete_*`). Force the Parseable UI for safety. Destructiveness is asymmetric.
14-
8. **Loud descriptions on side-effecting tools.** `evaluate_alert` warns "MAY TRIGGER REAL NOTIFICATIONS" so callers confirm with the user first.
15-
9. **Cross-platform first.** Tools-only over stdio is universal. Optional MCP features (elicitation, prompts, resources) only with a chat-Q&A fallback that works everywhere.
16-
17-
## Parseable API gotchas
18-
19-
Reference docs:
20-
- [API Reference](https://www.parseable.com/docs/api)
21-
- [Alerting](https://www.parseable.com/docs/user-guide/alerting)
22-
- [RBAC](https://www.parseable.com/docs/user-guide/rbac)
23-
- [PromQL](https://www.parseable.com/docs/user-guide/promql)
24-
- [Retention](https://www.parseable.com/docs/user-guide/retention)
25-
- [Installation modes (self-hosted)](https://www.parseable.com/docs/self-hosted/installation)
26-
- [SQL Editor](https://www.parseable.com/docs/user-guide/sql-editor)
27-
28-
Things we learned the hard way (not always obvious from docs):
29-
- Query payload uses **camelCase** keys. Required: `query`, `startTime`, `endTime`. Optional: `sendNull`.
30-
- Auth is **Basic** (username:password base64). Switch to PAT when Parseable ships it server-side.
31-
- All paths prefixed `/api/v1` - except PromQL.
32-
- **PromQL endpoints live under `/prometheus/api/v1/`** (different base). `start`/`end`/`time` params must be **unix epoch seconds** in practice - `query_promql` auto-converts RFC3339 → epoch before sending.
33-
- Cluster endpoints (`/cluster/info`, `/cluster/metrics`) only exist on **distributed** Parseable. Standalone returns 404. The `classifyStatus` helper surfaces a clear hint.
34-
35-
## File layout
3+
`parseable-mcp-server` is a thin MCP-to-Parseable adapter. Parseable owns tool
4+
definitions, validation, RBAC, execution, and edition-specific capabilities.
5+
This repository must not duplicate tool implementations.
366

37-
```
38-
src/
39-
├── server.ts # MCP wire-up, stdio transport, tool registration loop
40-
├── config.ts # env loader
41-
├── client.ts # Parseable HTTP client (Basic auth, AbortController timeout, error classification)
42-
└── tools/
43-
├── types.ts # ToolDef interface + jsonResult/errorResult helpers
44-
├── index.ts # tool registry - add new tools here
45-
└── <tool_name>.ts # one file per tool
46-
test/
47-
├── client.test.ts # parseErrorBody, classifyStatus, request behavior
48-
├── client_methods.test.ts # parametrized: every client method's verb + URL + body
49-
├── tools.test.ts # logic-heavy tools (query_sql, query_promql, explain_query)
50-
├── tools_passthrough.test.ts # every pass-through tool delegates correctly
51-
├── registry.test.ts # meta: unique names, snake_case, no destructive tools
52-
├── config.test.ts # env vars, defaults, overrides
53-
└── types.test.ts # jsonResult / errorResult helpers
54-
```
55-
56-
## Adding a tool
57-
58-
1. Create `src/tools/<name>.ts` exporting a `ToolDef`.
59-
2. Append to the array in `src/tools/index.ts`. The server auto-registers everything in that array.
60-
3. Add a HTTP method to `src/client.ts` if a new endpoint is involved.
61-
4. Add a test entry to `test/client_methods.test.ts` (verb + URL).
62-
5. Add a pass-through test to `test/tools_passthrough.test.ts` for boring tools, or a logic test in `test/tools.test.ts` for tools with their own behavior.
63-
6. `npm run lint && npm test && npm run build`.
64-
65-
Tool description guidelines:
66-
- Tell the caller *when* to use the tool, not just what.
67-
- Mention prerequisite tools (e.g. "Use `list_datasets` first to discover names").
68-
- Loud-warn on side effects: "MAY FIRE REAL NOTIFICATIONS", "Destructive - confirm first".
69-
- Keep description under ~500 chars unless steering a multi-step flow.
70-
71-
## TypeScript gotcha
7+
## Architecture
728

73-
`McpServer.registerTool` has deeply nested generic inference (`OutputArgs`/`InputArgs` union with `AnySchema`). Calling it inside a `for (const tool of tools)` loop triggers `TS2589: Type instantiation is excessively deep`. Workaround in `src/server.ts`: cast to a simpler function signature before invocation. Don't unwind into per-file registrations - the loop is fine, the cast is the right fix.
9+
```text
10+
MCP tools/list
11+
-> GET /api/prism/v1/llm/tools/list
7412
75-
## Lint + format - Biome
76-
77-
Single tool replaces ESLint + Prettier. Config in `biome.json`.
78-
79-
- 2-space indent, 100-char line width
80-
- Double quotes, semicolons, trailing commas
81-
- Imports auto-organized on `npm run fix`
82-
- `useImportType` (warn) - prefer `import type` for types
83-
- `useNodejsImportProtocol` (warn) - `node:fs` not `fs`
84-
- `noNonNullAssertion` (warn) - prefer `if (x)` narrowing over `x!`
85-
- `noExplicitAny` (warn)
13+
MCP tools/call
14+
-> POST /api/prism/v1/llm/tools/call
15+
```
8616

87-
Commands:
88-
- `npm run lint` - check only (CI uses this)
89-
- `npm run fix` - apply all safe auto-fixes (imports, formatting, lint rules)
90-
- `npm run format` - formatting only
17+
Registry responses are validated, cached per Parseable URL, tenant, and
18+
credential scope, then revalidated with `ETag`. Invocation results must be a
19+
valid MCP `CallToolResult` and are never cached.
9120

92-
Biome runs in ~10ms across the whole repo.
21+
Key files:
9322

94-
## Testing
23+
```text
24+
src/bootstrap.ts MCP tools/list and tools/call handlers
25+
src/client.ts Parseable registry and invocation HTTP client
26+
src/remote-tools.ts registry validation, LRU/TTL cache, ETag state
27+
src/http.ts stateless Streamable HTTP transport and request auth
28+
src/stdio.ts stdio transport
29+
src/cloud.ts Parseable Cloud API-key routing
30+
```
9531

96-
Vitest + native `fetch` mocking. Tests live in `test/*.test.ts`. Coverage via `npm run test:coverage` (v8 provider). Excluded: `src/server.ts` (boot wiring), `src/tools/index.ts` (registry - covered by `registry.test.ts`).
32+
## Adding or changing tools
9733

98-
Current: **95% statements / 98% functions / 95% lines / 108 tests**.
34+
Tool changes belong in the Parseable server catalog and executor. This adapter
35+
should change only when the REST or MCP contract changes.
9936

100-
## CI
37+
Required invariants:
10138

102-
`.github/workflows/ci.yml` runs on push + PR to main: `npm ci` → `npm run lint` → `npm run build:all` → `npm test`. Node 22.
39+
- Forward caller authentication and tenant routing on every upstream request.
40+
- Reject malformed registries, invalid schemas, and duplicate tool names.
41+
- Never cache tool invocation results.
42+
- Scope registry cache entries so credentials cannot share private catalogs.
43+
- Forward structured Parseable tool failures as MCP tool results.
44+
- Convert malformed upstream results and transport failures into MCP errors.
45+
- Do not log API keys, tool arguments, or tool results.
10346

104-
## Deliberately excluded - won't be accepted
47+
## Verification
10548

106-
- `ingest_event` - wrong shape for this protocol
107-
- `delete_*` - destructive, UI only
108-
- `create_user` / `create_role` / RBAC mutation - UI/CLI only
109-
- Live tail / streaming subscriptions - stdio transport not the right fit; use Parseable UI
49+
```bash
50+
npm run lint
51+
npm run build:all
52+
npm test
53+
```
11054

111-
## Releasing
55+
Tests use Vitest, native `fetch` mocks, and linked in-memory MCP transports.
56+
Add coverage for both REST mapping and complete MCP discovery/invocation flows.
11257

113-
Publishing a GitHub Release triggers `.github/workflows/release.yml`. The workflow verifies that the release tag matches the version in `package.json`, runs lint + build + test, then publishes the package to npm through trusted publishing with automatic provenance.
58+
## Style and release
11459

115-
Before the first release, configure a GitHub Actions trusted publisher in the package settings on npmjs.com. Use organization `parseablehq`, repository `parseable-mcp-server`, workflow `release.yml`, and allow `npm publish`. No npm token is required. Create releases with a `v`-prefixed tag matching `package.json` (for example, package version `0.3.0` requires tag `v0.3.0`).
60+
Biome owns formatting and linting (`npm run fix`). GitHub releases trigger npm
61+
publishing; release tag and `package.json` version must match.

‎README.md‎

Lines changed: 8 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,6 @@ For self-hosted mode, HTTP server validates supplied URL and forwards API key to
145145
|-----|----------|---------|---------|
146146
| `PARSEABLE_URL` | ✅ | — | Parseable base URL |
147147
| `PARSEABLE_API_KEY` | ✅ | — | API key for self-hosted Parseable |
148-
| `PARSEABLE_DEFAULT_DATASET` | | — | Advisory default dataset |
149-
| `PARSEABLE_MAX_ROWS` | | 1000 | Hard cap on query rows |
150148
| `PARSEABLE_QUERY_TIMEOUT_MS` | | 30000 | HTTP timeout (ms) |
151149

152150
### HTTP mode
@@ -178,67 +176,15 @@ Copy `.env.example` → `.env` for a full template.
178176

179177
## Tools
180178

181-
### Discovery
179+
Tools are owned by the connected Parseable instance, not this adapter. For
180+
MCP `tools/list`, the adapter fetches the edition-specific catalog from
181+
`GET /api/prism/v1/llm/tools/list`. OSS, Enterprise, and Cloud can expose
182+
different tool sets.
182183

183-
| Tool | Purpose |
184-
|------|---------|
185-
| `list_datasets` | List all log datasets |
186-
| `get_dataset_schema` | Column names + types |
187-
| `get_dataset_info` | Metadata (created_at, retention, time window) |
188-
| `get_dataset_stats` | Event count and storage bytes |
189-
| `sample_events` | Most recent N events (time-bounded, row-capped) |
190-
191-
### Query
192-
193-
| Tool | Purpose |
194-
|------|---------|
195-
| `query_sql` | SQL `SELECT` over a time window. DDL/DML blocked. Auto-injects `LIMIT`. |
196-
| `query_promql` | PromQL instant or range query against a metrics dataset |
197-
198-
### Alerts
199-
200-
| Tool | Purpose |
201-
|------|---------|
202-
| `list_alerts` | List all alerts with state, severity, tags |
203-
| `get_alert` | Full config for one alert |
204-
| `list_alert_tags` | All alert tags in use |
205-
| `enable_alert` | Enable an alert |
206-
| `disable_alert` | Disable an alert |
207-
| `evaluate_alert` | Force-evaluate now. **May fire real notifications.** |
208-
| `create_alert` | Create alert via guided Q&A (8 questions, confirms before submit) |
209-
210-
### Alert targets
211-
212-
| Tool | Purpose |
213-
|------|---------|
214-
| `list_alert_targets` | List targets (Slack, webhook, Alertmanager) |
215-
| `get_alert_target` | Full config for one target |
216-
| `create_alert_target` | Create a new Slack / webhook / Alertmanager target |
217-
218-
### Diagnostics
219-
220-
| Tool | Purpose |
221-
|------|---------|
222-
| `ping` | Check connectivity, return version + health |
223-
| `explain_query` | `EXPLAIN` a SQL query without executing it |
224-
225-
### RBAC (read-only)
226-
227-
| Tool | Purpose |
228-
|------|---------|
229-
| `list_users` | List all users |
230-
| `get_user_roles` | Roles for a specific user |
231-
| `list_roles` | All role names |
232-
| `get_role` | Privilege definition for a role |
233-
| `get_default_role` | Default role for new users |
234-
235-
### Admin (read-only)
236-
237-
| Tool | Purpose |
238-
|------|---------|
239-
| `get_cluster_status` | All nodes with status (distributed mode) |
240-
| `get_cluster_metrics` | Aggregated ingest/query/storage metrics |
241-
| `get_retention` | Retention policy for a dataset |
184+
The adapter caches the catalog for its server-provided `max-age` and
185+
revalidates it with `ETag`. MCP `tools/call` requests are forwarded to
186+
`POST /api/prism/v1/llm/tools/call`, where Parseable validates arguments,
187+
applies RBAC, executes the tool, and returns an MCP `CallToolResult`.
242188

243189
---
244190

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@parseable/parseable-mcp-server",
3-
"version": "0.2.16",
3+
"version": "0.3.0",
44
"mcpName": "io.github.parseablehq/parseable-mcp-server",
55
"description": "Model Context Protocol server for Parseable. Lets LLMs discover and query Parseable datasets.",
66
"repository": {
@@ -18,9 +18,11 @@
1818
"LICENSE"
1919
],
2020
"scripts": {
21-
"build": "tsc && chmod +x dist/server.js",
21+
"clean": "node scripts/clean.mjs",
22+
"build": "npm run clean && npm run build:server",
23+
"build:server": "tsc && chmod +x dist/server.js",
2224
"build:ui": "vite build",
23-
"build:all": "npm run build:ui && npm run build",
25+
"build:all": "npm run clean && npm run build:ui && npm run build:server",
2426
"dev": "tsc --watch",
2527
"dev:ui": "vite",
2628
"start": "node dist/server.js",
@@ -30,7 +32,7 @@
3032
"lint": "biome check src test",
3133
"format": "biome format --write src test",
3234
"fix": "biome check --write src test",
33-
"prepublishOnly": "npm run build && npm run lint && npm test"
35+
"prepublishOnly": "npm run build:all && npm run lint && npm test"
3436
},
3537
"dependencies": {
3638
"@hono/node-server": "^2.0.12",

‎scripts/clean.mjs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { rmSync } from "node:fs";
2+
import { resolve } from "node:path";
3+
4+
rmSync(resolve("dist"), { recursive: true, force: true });

0 commit comments

Comments
 (0)