Skip to content

MCP Endpoint

The /mcp endpoint turns NimbleBrain into a Streamable HTTP MCP server. External MCP clients — Claude Desktop, Claude Code, Cursor, Open WebUI, another NimbleBrain instance, or any MCP-compatible host — connect here and access every tool installed in the workspace.

This makes NimbleBrain both an MCP client (connecting to installed bundles) and an MCP server (exposing composed tools to external hosts). The composition layer in between — tool aggregation, feature gating, role filtering, workspace scoping — is what external clients benefit from without needing to know about individual bundles.

For end-user client configuration walkthroughs, see Connecting External Clients.

Path/mcp
MethodsPOST, GET, DELETE
ProtocolStreamable HTTP (MCP transport)
AuthOAuth 2.0 via WorkOS AuthKit (JWT bearer) — discovered via RFC 9728
ScopingEvery request is scoped to a workspace via the X-Workspace-Id header

The three methods correspond to the Streamable HTTP session lifecycle:

  • POST — initialize a session or send subsequent JSON-RPC messages
  • GET — open the SSE stream for server-initiated messages (uses mcp-session-id)
  • DELETE — terminate a session
Terminal window
claude mcp add --transport http nimblebrain https://your-instance/mcp \
--header "X-Workspace-Id: ws_your_workspace_id"

Or via .mcp.json:

.mcp.json
{
"mcpServers": {
"nimblebrain": {
"type": "http",
"url": "https://your-instance/mcp",
"headers": {
"X-Workspace-Id": "ws_your_workspace_id"
}
}
}
}

The first request triggers the OAuth flow — Claude Code opens a browser window for you to authenticate against AuthKit, then attaches the returned JWT on every subsequent request.

Authentication is OAuth-based. There is no static API key to copy around.

The first time a client hits /mcp without a valid JWT, it receives a 401 with a WWW-Authenticate header:

Bearer error="unauthorized",
error_description="Authorization required",
resource_metadata="https://your-instance/.well-known/oauth-protected-resource"

Clients that implement RFC 9728 (Protected Resource Metadata) and RFC 8414 (Authorization Server Metadata) — Claude Code, Claude Desktop, and Cursor all do — follow the discovery chain automatically, open a browser for user login against AuthKit, and attach the returned JWT on subsequent requests.

Every MCP request is scoped to a workspace. External clients identify the workspace with the X-Workspace-Id request header.

Workspace IDs have the form ws_<alphanumeric> and can be copied from Settings → Profile → MCP Connection in the web UI.

If the header is missing, the server returns 400 with the message “Workspace required. Set the X-Workspace-Id header. The workspace ID is available from GET /v1/bootstrap or Settings → Profile → MCP Connection.” — regardless of how many workspaces the user belongs to. The server does not pick a default on data endpoints.

The MCP endpoint exposes:

  • All tools from installed and running bundles in the workspace — aggregated into the unified tool namespace
  • Tool discoverytools/list returns every tool visible to the caller
  • Tool executiontools/call routes to the correct bundle transparently

Two filters apply to both tools/list and tools/call:

  • Feature flags — tools whose controlling flag is false are excluded (e.g., nb__manage_app when bundleManagement: false). See Feature Flags.
  • Role — admin-only tools (nb__manage_users, nb__manage_workspaces, nb__set_model_config) are excluded when the caller’s org role is not admin or owner.

Each initialize creates a new MCP session tracked server-side.

SettingEnv varDefaultDescription
Max concurrent sessionsMCP_MAX_SESSIONS100New initialize returns 429 once this is reached.
Session TTLMCP_SESSION_TTL_SECONDS28800 (8 h)Idle timeout. Each request resets the clock — an actively-used session never expires.

Sessions are also closed on DELETE /mcp (with a valid mcp-session-id), transport close, or server shutdown.

Session metadata (which sessions exist, when they were last touched) lives behind a pluggable provider. The default is process-local in-memory; multi-replica deploys use a shared backend so sessions are visible cluster-wide.

See sessionStore config for shape, providers, and operator guidance.

When the server can’t serve a request because the session isn’t available, the 404 response includes error.data.reason:

{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Session not found",
"data": { "reason": "not_found" }
},
"id": null
}
ReasonMeaning
not_foundSession store has no record. Most often: idle-TTL eviction, or the session never existed.
unavailableSession exists in the store, but the live transport isn’t on this process. Possible causes: process restart since initialize, sticky-routing miss, or local transport closure.

Spec-compliant clients re-initialize on either reason. The field exists for diagnostic purposes — operators see it in logs ([mcp] session miss reason=...); tooling can use it to tell steady-state idle eviction apart from process/routing issues.

The endpoint is gated by the mcpServer feature flag:

nimblebrain.json
{
"features": {
"mcpServer": true
}
}

When mcpServer is false, all requests to /mcp return 404 Not found. Set it to false if you don’t need external MCP access.

In dev mode (no auth), you can hit the endpoint with curl:

Terminal window
# Initialize a session
curl -i -X POST http://localhost:27247/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-Workspace-Id: ws_dev" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "curl", "version": "1.0" }
}
}'

Extract the mcp-session-id response header and use it on subsequent calls:

Terminal window
curl -X POST http://localhost:27247/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-Workspace-Id: ws_dev" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}'

With auth enabled, include Authorization: Bearer <jwt> on every request.

External MCP Client NimbleBrain
───────────────── ─────────────
┌──────────────────────┐
POST /mcp (initialize) ───►│ /mcp endpoint │
│ (Streamable HTTP) │
GET /mcp (SSE) ────►│ session manager │
DELETE /mcp (close) ────►│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ ToolRegistry │ │
│ │ (workspace- │ │
│ │ scoped) │ │
│ │ │ │
│ │ App 1 tools │ │
│ │ App 2 tools │ │
│ │ System tools │ │
│ └──────────────┘ │
└──────────────────────┘

The external client sees a flat list of tools. NimbleBrain resolves the caller’s identity, picks the right workspace registry, and routes each tools/call to the correct bundle process.

  • JWT verification — access tokens are verified against AuthKit’s JWKS endpoint; RS256-signed with issuer validation.
  • Organization scoping — when organizationId is set, only members of that WorkOS organization can authenticate.
  • Workspace isolation — every request is scoped to one workspace; tools cannot reach data in other workspaces.
  • Feature and role filtering — disabled and admin-only tools are never exposed.
  • Session limitsMCP_MAX_SESSIONS prevents resource exhaustion from runaway clients.
  • Bundle env isolation — bundle processes receive a filtered host environment; the MCP endpoint does not bypass that.

The sections below are for operators deploying a NimbleBrain instance — not for client developers connecting to an existing one.

To enable OAuth, set authkitDomain in instance.json under the WorkOS auth adapter:

instance.json
{
"auth": {
"adapter": "workos",
"clientId": "client_...",
"redirectUri": "https://your-instance/v1/auth/callback",
"organizationId": "org_...",
"authkitDomain": "your-subdomain"
}
}

authkitDomain is the subdomain part of your AuthKit URL (your-subdomain.authkit.app"your-subdomain"). In the WorkOS Dashboard, enable Client ID Metadata Document (CIMD) under Connect → Configuration so MCP clients can self-identify without prior registration.

NimbleBrain serves two unauthenticated discovery endpoints that MCP clients fetch during the OAuth flow:

GET /.well-known/oauth-protected-resourceRFC 9728 Protected Resource Metadata.

{
"resource": "https://your-instance",
"authorization_servers": ["https://your-subdomain.authkit.app"],
"bearer_methods_supported": ["header"]
}

GET /.well-known/oauth-authorization-serverRFC 8414 Authorization Server Metadata, proxied from AuthKit for backward compatibility with older MCP clients.

By default, AuthKit hosts the login page users see during the OAuth flow. If you want MCP clients to authenticate through your own login UI instead, configure Standalone Connect in the WorkOS Dashboard:

  1. AuthKit redirects the user to your configured Login URI.
  2. Your application authenticates the user through its existing system.
  3. Your application calls AuthKit’s completion API to finalize the OAuth flow.
  4. AuthKit issues tokens and returns control to the MCP client.

NimbleBrain’s side of the flow (JWKS verification, /mcp bearer handling, discovery endpoints) works identically — only the login UI changes.

The OAuth resource URL advertised on /.well-known/oauth-protected-resource must match the scheme the client connected on. If you terminate TLS at an upstream proxy, see Security → MCP OAuth behind a reverse proxy for the X-Forwarded-Proto configuration required per proxy (ALB, nginx, Caddy).