Skip to content

The MCP Endpoint

Each workspace has its own Streamable HTTP MCP server at /mcp/<workspaceId>. It is NimbleBrain’s public programmatic surface: external MCP clients (Claude, Claude Code, Cursor, another NimbleBrain instance, or any MCP-compatible host) connect to one workspace’s URL and reach that workspace’s tools.

The runtime also serves a /v1/* REST API, but that is a trusted surface for the bundled web client and for apps the operator runs and lists as first-party clients — not a supported external integration point. For programmatic access from outside the platform, use the MCP endpoint.

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

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

URL <origin>/mcp/<workspaceId> — for example https://nb.example.com/mcp/ws_0123456789abcdef
Methods POST (GET and DELETE return 405)
Protocol Streamable HTTP (MCP transport), revision 2026-07-28 only — see Protocol versions
Auth OAuth 2.0 bearer token bound to this URL (RFC 8707), discovered via RFC 9728
Scope One workspace — the one in the URL — plus your identity tools

Every member sees the workspace’s URL under Workspace settings → MCP, with a copy button. <origin> is the instance’s configured public origin (its custom domain, when it has one), not whichever host you happen to reach it on.

The methods:

  • POST — every JSON-RPC message, each request on its own under the 2026-07-28 _meta envelope (see Protocol versions).
  • GET, DELETE — return 405 Method Not Allowed. There is no session to end, and NimbleBrain does not offer the optional GET-style server-to-client SSE channel; progress flows over the POST that started the call.

The workspace in the URL is the gate. Every request checks that the caller is a member of it, and a workspace you are not a member of answers exactly like one that does not exist: 404 Workspace not found. The endpoint never tells a caller whether a workspace exists.

Which credentials reach the endpoint:

Credential At /mcp/<workspaceId>
A token from the MCP authorization server Accepted only if its aud is exactly this workspace’s URL (see below), and you are a member.
The web app’s own login session Accepted for a member. It was issued to the instance itself, not minted for a resource, so membership alone gates it.
No credential 401 with a WWW-Authenticate header pointing at this workspace’s discovery document.

A token from the authorization server is valid only for the resource it was minted for. At /mcp/<workspaceId> its aud claim (a string, or any element of an array) must equal the workspace’s canonical URL, <origin>/mcp/<workspaceId>, character for character. There is no prefix match and no normalization, so each of these is refused with 401:

  • the bare origin, or the environment’s OAuth client id (what a token minted without a resource carries);
  • the URL with a trailing slash, a path below it, or a host in a different case;
  • another workspace’s URL.

A token minted for a workspace URL is refused on every /v1/* REST route as well.

The audience stops a token being replayed somewhere it was not minted for; it does not grant access. Membership of the workspace does, checked on every request — so a token whose audience names a workspace you have left gets 404.

Adapter configuration (dev, oidc, or workos) lives in instance.json. See auth adapters for the full setup, including the workos adapter and authkitDomain required to enable MCP OAuth.

With the dev adapter every request authenticates as the single local developer, and the workspace in the URL must still exist and have that developer as a member.

A 401 from /mcp/<workspaceId> carries:

Bearer error="unauthorized",
error_description="Authorization required",
resource_metadata="https://nb.example.com/.well-known/oauth-protected-resource/mcp/<workspaceId>"

Clients that implement RFC 9728 (Protected Resource Metadata) and RFC 8707 (Resource Indicators) — Claude, Claude Code, and Cursor all do — follow the discovery chain automatically, open a browser for login, and request a token for the resource the document names.

Both discovery endpoints return 404 MCP OAuth not configured unless the workos adapter has an authkitDomain set.

GET /.well-known/oauth-protected-resource/mcp/<workspaceId>

Section titled “GET /.well-known/oauth-protected-resource/mcp/<workspaceId>”

Protected Resource Metadata for one workspace’s endpoint, at the path RFC 9728 §3.1 derives from its URL:

{
"resource": "https://nb.example.com/mcp/ws_0123456789abcdef",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"]
}

resource is built from the configured public origin, never from the request’s Host or X-Forwarded-* headers. The document is served before authentication, so it answers for any well-formed workspace id without looking the workspace up.

The root document, /.well-known/oauth-protected-resource, returns 404: the origin itself is no resource that accepts a token, so advertising it would send clients to mint tokens every route refuses.

GET /.well-known/oauth-authorization-server

Section titled “GET /.well-known/oauth-authorization-server”

RFC 8414 Authorization Server Metadata, proxied from the authorization server for older MCP clients that look for this endpoint instead of Protected Resource Metadata.

  1. Client POSTs to /mcp/<workspaceId> with no token and gets 401 + WWW-Authenticate carrying the workspace’s resource_metadata URL.
  2. Client fetches that document to learn the authorization server and the canonical resource.
  3. Client runs the OAuth authorization-code flow with resource=<origin>/mcp/<workspaceId>, opening a browser for login.
  4. The authorization server issues a JWT whose aud is that URL; the client attaches it as Authorization: Bearer <jwt> on every request thereafter.

NimbleBrain verifies each access token’s signature against the authorization server’s JWKS (RS256, issuer validation), then applies the audience rule and any restriction the auth adapter is configured with, such as the workos adapter’s organizationId.

Operator requirement: a resource indicator per public host

Section titled “Operator requirement: a resource indicator per public host”

The authorization server honours a client’s resource only for URLs it has been configured to accept. For every public host the instance is reached on — its platform host and any custom domain — register a resource indicator covering <origin>/mcp/*. Without one, the authorization server ignores resource, mints a token whose audience is the client id, and every MCP request is refused with 401.

The endpoint speaks MCP 2026-07-28 and nothing earlier. Every request carries its protocol version, client info and client capabilities in its own params._meta envelope, and is authenticated, membership-checked and answered on its own: there is no initialize, no Mcp-Session-Id and no server-side session, so any process can answer any request. A client probes with server/discover, which answers the server’s capabilities.

A request without the envelope, including a 2025-era initialize, is refused with HTTP 400 and JSON-RPC error -32022 (unsupported protocol version). error.data.supported names the revision served, and error.data.requested echoes the version the request named, when it named one. An initialize offering 2025-11-25 is answered:

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version: 2025-11-25",
"data": { "supported": ["2026-07-28"], "requested": "2025-11-25" }
}
}

A client that only speaks a 2025 revision must be upgraded to connect. The runtime logs each refused client’s User-Agent once per process ([mcp] refused a pre-2026-07-28 client userAgent=…), so the clients that need upgrading show in the logs.

The endpoint advertises neither listChanged nor resources.subscribe, so a client has nothing to open a subscriptions/listen stream for; a resources/read whose URI resolves nowhere answers -32602.

Long-running tools run as tasks through the MCP tasks extension (io.modelcontextprotocol/tasks), advertised in server/discover. A tools/call that names the extension in its request’s client capabilities is answered with a task (resultType: "task") when the tool can run as one. Poll with tasks/get, which inlines the result once the task ends, and cancel with tasks/cancel. Each poll is a request on its own, with the task id as its Mcp-Name header. The 2025-11-25 tasks utility is not served.

A task is reachable only by the identity that started it, at the workspace URL it was started at; any other poll is answered -32602, the same as a task that does not exist.

For the workspace in the URL, the endpoint exposes:

  • Workspace tools — <source>__<tool>, from the connectors installed and running in that workspace. The workspace is not in the name: it is the one in the URL.
  • Identity tools — your conversations, files, and tasks (bare <source>__<tool>), over that workspace’s data.
  • Instructions — server/discover answers the platform’s own instructions, then each connector’s, under its source name, so a client’s model learns how the workspace’s tools are meant to be used.
  • Skills — the skills the workspace’s connectors serve, through the MCP Skills extension (skills/list, skills/get, and resources/read for each file).
  • Tool execution — tools/call runs a tool in that workspace or an identity tool. A name cannot address a different workspace; the retired ws_<id>- form is refused with -32602 invalid_tool_name. A bare name resolves against that workspace’s sources; your personal connectors are reachable only under their my_-marked names. A bare name naming neither is refused with -32602 unknown_tool_source.

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

  • Feature flags — tools whose controlling flag is false are excluded (for example, the skills__* authoring tools when skillManagement is false). See Feature Flags.
  • App-only tools — tools whose visibility is ["app"] (org administration such as nb__manage_users and nb__manage_workspaces, and model configuration) are excluded, and a tools/call naming one is refused unless an app view inside NimbleBrain makes it. A call counts as the view’s when it names its app’s source and carries a first-party credential, so this keeps app-only tools from agents, not from someone holding the person’s own sign-in. Under oidc, where every token is the web login’s own (see OIDC adapter), a client using that sign-in can call them as the view does. Under dev, which checks no credential, every request is the developer with first-party standing, so any caller that reaches the endpoint can name a source and call them.
  • Role — a tool limited to org admins is excluded when the caller’s role is not admin.

With the dev adapter, you can hit the endpoint with curl. Take a workspace id from Workspace settings → MCP:

Every request carries the _meta envelope in its params and the MCP-Protocol-Version and Mcp-Method headers. Discover the server’s capabilities:

Terminal window
curl -X POST http://localhost:27247/mcp/<workspaceId> \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "curl", "version": "1.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'

Then list the tools. Each request stands alone, so it carries the same envelope:

Terminal window
curl -X POST http://localhost:27247/mcp/<workspaceId> \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/list" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "curl", "version": "1.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'

A tools/call also sends the tool’s name as the Mcp-Name header (for example -H "Mcp-Name: <source>__<tool>"), matching params.name.

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