Berserk Docs

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:

  1. POST to /mcp and receive 401 with WWW-Authenticate pointing at Protected Resource Metadata
  2. Discover this cluster's authorization server
  3. Open a browser for login and consent (authorization-code + PKCE)
  4. 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:

  • Progressnotifications/progress only when _meta.progressToken is set. Coverage percent, monotonic, throttled. Hosts show this on the spinner.
  • Sampled snapshots — a few notifications/message events (first iteration, coverage marks, signal jumps, stop), and only when _meta carries io.modelcontextprotocol/logLevel at debug or info. Without that field there are none: MCP 2026-07-28 forbids a server from emitting notifications/message for a request that did not ask. A stricter level (notice and above) also silences them, since snapshots are info. Progress is gated separately by progressToken. 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.
  • resultType is "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.

On this page