MCP
Connect Claude Code, Cursor, and other MCP hosts to Berserk over HTTP
Berserk exposes a remote Model Context Protocol server at https://<your-cluster-endpoint>/mcp. It speaks MCP 2026-07-28 only (no initialize, no session, no GET stream). Authentication is ordinary HTTP OAuth — there is no bzrk CLI in the path.
Older MCP is not supported
Agent hosts pick up protocol revisions quickly, so Berserk does not dual-speak older MCP (initialize, sessions, GET SSE). Request-scoped SSE, _meta.progressToken progress, stop_when, and timeout_ms only exist on the 2026 transport — carrying 2025-11-25 would mean shipping a second protocol that cannot offer them.
A host that still sends initialize gets JSON-RPC -32022 with data.supported: ["2026-07-28"]. Use Claude Code 2.1.232+ or another 2026-capable HTTP MCP host. Until yours catches up, use the CLI + skill path.
Configure the host
Point the MCP host at the cluster URL. Do not configure a stdio command such as bzrk mcp — that command is gone.
{
"mcpServers": {
"berserk": {
"type": "http",
"url": "https://<your-cluster-endpoint>/mcp"
}
}
}Claude Code (2.1.232+) HTTP MCP, MCP Inspector, and other hosts that implement MCP HTTP auth will:
- POST to
/mcpand receive401withWWW-Authenticatepointing at Protected Resource Metadata - Discover this cluster's authorization server
- Open a browser for login and consent (authorization-code + PKCE)
- Store the Bearer token and call tools
After that, server/discover, tools/list, and tools/call work as ordinary authenticated POSTs.
Locally (mise run dev ui), the same URL is http://127.0.0.1:9500/mcp — the gateway, not the UI hop on :9570. Use 127.0.0.1, not localhost: dual-stack ::1 misses the IPv4 bind.
Service principal token (no browser)
Hosts that cannot run the OAuth dance can send a service-principal token you mint in Settings → service principals:
{
"mcpServers": {
"berserk": {
"type": "http",
"url": "https://<your-cluster-endpoint>/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}A bzrk login access token works the same way, but it expires in an hour unless the host refreshes it. Service-principal tokens last until you revoke them (or until an expiry you set). Browser session cookies are rejected.
bzrk mcp is gone
Do not add a stdio MCP server that runs bzrk mcp. Multi-profile switching through the CLI is not supported. Use the remote URL above, or the CLI + skill path if you want the agent to shell out to bzrk search.
Query
The query tool runs KQL against a database and time range (default 1h ago..now). Each run is saved and returns a run_id. Discover schema first (list_databases, list_tables, discover_fields) — field names are not guessable.
Two optional arguments share the stop language used by CLI --stop-when and workflow “Continue when”. They cancel at a complete result iteration, never a torn batch. MCP does not offer --stop-cmd.
stop_when
Predicate grammar: <ident> <op> <number> — ident is rows (snapshot row count) or a column name (first row’s numeric value); op is one of >=, <=, >, <, ==, !=. Invalid strings are a tool error.
Threshold — cancel as soon as a complete snapshot proves “at least 50”:
{
"query": "default | where severity_text == 'ERROR' | summarize n=count()",
"database": "default",
"since": "7d ago",
"stop_when": "n >= 50"
}Existence — rows >= 1 / rows > 0 on a row-producing, unlimited, non-aggregate query also injects | take 1, so the engine stops scheduling. Pipelines that cannot legally be extended (render, otel-log-stats, a trailing ;) are left alone, same as the CLI. The result is a single row (“does a match exist?”):
{
"query": "default | where severity_text == 'ERROR'",
"database": "default",
"since": "7d ago",
"stop_when": "rows >= 1"
}A column predicate reads only the first row. Sort that column to the top to express “any row crosses X”:
{
"query": "default | summarize n=count() by service | sort by n desc",
"database": "default",
"since": "6h ago",
"stop_when": "n > 1000"
}The result is a partial lower bound over the requested window — exactly what a threshold or existence question needs. == / !=, absence, and exact min/max/count over the whole window do not cancel; they need a full scan (or they run until timeout_ms).
timeout_ms
Default wait is 10 seconds — a nudge, not a wall. Ceiling is the configured cap (300 seconds by default). At the deadline the last complete iteration comes back as an honest partial (status: "soft_budget", with coverage), not a failed RPC: narrow since, add a filter, pass stop_when, or raise timeout_ms. If you set timeout_ms, that value is the wait (it is not also cut at 10s). An absorbing stop_when that fires first still wins. A timeout_ms above the cap is an error, not a silent clamp.
MCP does not reject a wide first window; the 10s default is the cost pressure. A partial that cites coverage means the query was too expensive.
Structured status on the final result is one of complete | stopped_early | soft_budget.
Streaming (SSE)
If the host’s Accept includes text/event-stream, query is request-scoped SSE (MCP 2026-07-28): notifications first, the JSON-RPC response last, then the stream ends. JSON-only Accept still honors stop_when and timeout_ms; there is just no mid-flight channel.
SSE is progress and status, not a table firehose:
- Progress —
notifications/progressonly when_meta.progressTokenis set. Coverage percent, monotonic, throttled. Hosts show this on the spinner. - Sampled snapshots — a few
notifications/messageevents (first iteration, coverage marks, signal jumps, stop), and only when_metacarriesio.modelcontextprotocol/logLevelatdebugorinfo. Without that field there are none: MCP 2026-07-28 forbids a server from emittingnotifications/messagefor a request that did not ask. A stricter level (noticeand above) also silences them, since snapshots areinfo. Progress is gated separately byprogressToken. Never every iteration. Hosts may render them; many will not put them in the model context. The model’s contract is the final tool result arriving sooner because we stopped. resultTypeis"complete"on every finished RPC. That field means the call finished, not that the scan covered 100% of the window. Coverage lives in structured content (partial,coverage,status).
Close the stream to cancel
Dropping the SSE response cancels the query. There is no GET stream, no Last-Event-ID, and no subscriptions/listen for live results — get_query_result is the fetch-later path.