Skip to main content

mcp

Model Context Protocol transport for Webda — exposes registered operations as MCP tools and models as MCP resources, over Streamable HTTP and stdio.

When to use it​

  • You want AI assistants (Claude Code, Claude Desktop, any MCP client) to call your Webda operations as tools.
  • You want models readable as MCP resources through webda://{Model}/{pk...} URIs.
  • You want the same permissions as your REST API: each caller only sees and runs what their session allows.

What it exposes​

  • Tools: every non-hidden operation, the tool name being the operation id (for example Post.Get, Posts.Query, User.Login).
  • Resources: models with a {Model}.Get operation, as webda://{Model}/{pk...} resource templates (for example webda://Post/{slug}).
  • Progress: AsyncGenerator operations stream their chunks as MCP progress notifications.
  • Truncated outputs: tool outputs larger than maxOutputBytes are truncated with a notice and returned with isError: true.

Install​

pnpm add @webda/mcp

Configuration​

{
"services": {
"mcp": {
"type": "Webda/McpService",
"url": "/mcp", // HTTP endpoint; false disables HTTP (stdio only)
"operations": ["*", "!User.Delete"], // standard OperationsTransport filter
"resources": { "models": ["*"] }, // false disables resources; model ids or "*"
"sessionTimeout": 1800, // seconds of inactivity before an MCP session is evicted
"allowedOrigins": [], // extra Origin values accepted besides the request host
"allowedHosts": ["localhost", "127.0.0.1", "::1"], // Host header hostnames accepted (any port); "*" disables the check
"maxSessions": 1000, // cap on open MCP HTTP sessions; at the cap the least recently used one is closed
"maxOutputBytes": 1048576 // larger tool/resource outputs are truncated with a notice
// "serverName": "my-app", // defaults to the application package name
// "serverVersion": "1.0.0", // defaults to the application package version
// "stdio": { "user": "<userId>" }, // default --user for `webda mcp`
// "authenticator": "myAuthenticator", // service implementing McpAuthenticator; default uses the Webda session
// "listOutOfScopeTools": false // list the tools an OAuth token scope does not cover
}
}
}
ParameterTypeDefaultDescription
urlstring | false"/mcp"HTTP endpoint; false disables HTTP (stdio only)
operationsstring[]["*"]Operation filter, with ! for exclusions
resources{ models: string[] } | false{ "models": ["*"] }Models exposed as resources; false disables resources
sessionTimeoutnumber1800Seconds of inactivity before an MCP session is evicted
allowedOriginsstring[][]Extra Origin values accepted besides the request host
allowedHostsstring[]["localhost", "127.0.0.1", "::1"]Hostnames accepted on the Host header (any port); "*" disables the check
maxSessionsnumber1000Cap on open HTTP sessions; at the cap a new initialize closes the least recently used session, whose next request gets 404 (clients then re-initialize)
maxOutputBytesnumber1048576Larger tool/resource outputs are truncated with a notice
serverNamestringapplication package nameServer name announced to clients
serverVersionstringapplication package versionServer version announced to clients
stdio.userstringnoneDefault --user for webda mcp
authenticatorstringnoneName of a service implementing McpAuthenticator; the default uses the Webda session
listOutOfScopeToolsbooleanfalseWith McpOAuthAuthenticator, also list the tools the token scope does not cover (see OAuth)

Remote deployments must set allowedHosts. The Host header is checked on every HTTP request as DNS-rebinding protection: unless your public hostname(s) are listed (or the value is "*"), every request gets 403 Host not allowed.

Behind a reverse proxy, the hostname checked is the X-Forwarded-Host header when present, otherwise Host. X-Forwarded-* headers are only accepted from proxies listed in the HttpServer trustedProxies parameter (anything else gets 400 from the HttpServer). A proxy that rewrites Host without a trusted X-Forwarded-Host gets 403 unless the rewritten hostname is in allowedHosts.

Usage​

# stdio: Claude launches the application and acts as the given user
claude mcp add blog -- webda mcp --user <userId>

# HTTP: register the endpoint of a running application
claude mcp add --transport http blog http://localhost:18080/mcp

Use https:// when the HttpServer serves TLS. With autoTls (as in the blog sample) the certificate is self-signed: start the MCP client with NODE_EXTRA_CA_CERTS=<app>/.webda/dev-tls/cert.pem so it trusts it.

webda mcp keeps stdout for protocol frames: from the moment the command starts, log lines and any other stdout output go to stderr, whatever --log-stream says. Lines logged while the application boots (before the command starts) follow --log-stream; its default (auto) already sends them to stderr because MCP clients pipe stdout.

Who is the HTTP caller?​

The default SessionAuthenticator uses Webda's cookie session. MCP clients (Claude Code, the SDK HTTP transports) do not keep cookies, so HTTP callers are anonymous in practice: they see and run only the operations anonymous users may call. To act as a user, use stdio (webda mcp --user <userId>), use OAuth so MCP clients act as the consenting user, or plug a custom authenticator service:

import { Service, Session, WebContext, WebdaError } from "@webda/core";
import type { McpAuthenticator } from "@webda/mcp";

// Your own key lookup (a model query, a secret store...)
declare function lookupUserForKey(key: string): Promise<string | undefined>;

/**
* Resolve MCP callers from an API key header
*/
export class ApiKeyAuthenticator extends Service implements McpAuthenticator {
async authenticate(ctx: WebContext): Promise<Session> {
const key = ctx.getHttpContext().getUniqueHeader("x-api-key");
const session = new Session();
if (key === undefined) {
return session; // anonymous
}
const userId = await lookupUserForKey(key);
if (!userId) {
throw new WebdaError.Unauthorized("Invalid API key");
}
session.login(userId, userId);
return session;
}
}
{
"services": {
"apiKeys": { "type": "MyApp/ApiKeyAuthenticator" },
"mcp": { "type": "Webda/McpService", "authenticator": "apiKeys" }
}
}

OAuth​

McpOAuthAuthenticator makes the MCP endpoint an OAuth 2.1 protected resource (RFC 9728) for tokens issued by @webda/oauth. MCP clients discover the authorization server, register themselves, send the user through consent and then call with a scoped bearer token bound to this endpoint. Step-by-step client setup: Connecting agents.

{
"services": {
"McpOAuthAuthenticator": {
"type": "Webda/McpOAuthAuthenticator",
"publicUrl": "https://api.example.com"
},
"Mcp": { "type": "Webda/McpService", "authenticator": "McpOAuthAuthenticator" }
}
}
Parameter (authenticator)DefaultDescription
publicUrlnone (request host, development only)Public origin of the application, including any external path prefix
authorizationServerspublicUrl or the request originAuthorization servers advertised in the metadata
resourceMetadataPath/.well-known/oauth-protected-resourcePath of the protected resource metadata
scopesSupported*, <Group>.*, offline_accessScopes advertised in the metadata, like the Webda authorization server

Audience binding. The resource is publicUrl + the McpService url (https://api.example.com/mcp above). It is registered with the OAuthServer, advertised in the metadata, and every token must carry it as audience (RFC 8707): an application token (aud webda-access) or a token issued for another resource is refused with 401 and WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…", so the client signs in again.

  • publicUrl must be the URL clients use, including any path prefix added in front of the application (an API Gateway stage such as https://abc.execute-api.eu-west-1.amazonaws.com/prod, a reverse proxy mount point...).
  • Without publicUrl the resource is derived from the request Host (or a trusted X-Forwarded-Host) and a warning is logged at startup. The audience check then follows whatever host the request names: it is not a security boundary, use it for development only.
  • The metadata is served at /.well-known/oauth-protected-resource (resourceMetadataPath), with the RFC 9728 path-suffixed form when several MCP endpoints share the authenticator.
  • Without a token the endpoint answers 401 with WWW-Authenticate: Bearer resource_metadata="…", which starts the client sign-in.

Scope. The token scope limits the tools a client sees and calls, on top of the user's own permissions:

  • Tools outside the token scope are hidden from tools/list by default. Calling one anyway answers HTTP 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="<granted scope> <operation>", resource_metadata="…" and the JSON-RPC error -32001 (insufficient_scope, the same scope in data). A client re-authorizes with that scope: the consent screen shows the operations already granted as "(already allowed)" and the new grant adds to them. In a JSON-RPC batch the error is carried in the body (HTTP 200).
  • listOutOfScopeTools: true on the McpService also lists the tools the user may call but the token scope does not cover, so an agent can discover them and step up when it needs one. Default false.
  • Tools the user cannot call because of permissions are never listed and answer Unknown tool, as for a tool that does not exist; so do the OAuth.* operations, which are never delegated.

Multiple instances​

MCP HTTP sessions live in the memory of the instance that created them. When running several instances behind a load balancer, route each client to the same instance (sticky sessions on the Mcp-Session-Id header or the client address); a request reaching another instance gets 404 Session not found.

Control how an operation appears to MCP clients:

import { Operation, OperationContext, Service } from "@webda/core";

export class PostService extends Service {
// Not exposed as a tool
@Operation({ mcp: false })
async internalCleanup() {}

// Exposed with a title and explicit hints
@Operation({ mcp: { title: "Delete a post", readOnly: false, destructive: true } })
async deletePost(ctx: OperationContext) {}
}

Security​

  • Permissions apply per caller: an operation is run with the caller's session and tools/list is filtered to what that caller may call.
  • HTTP sessions are random ids bound to the user who created them; another user presenting the id is rejected with 403.
  • Every HTTP request is checked against allowedHosts and Origin (request host plus allowedOrigins) to prevent DNS rebinding.
  • Open sessions are capped by maxSessions; at the cap the least recently used session is closed rather than refusing new ones, so nobody can lock the endpoint by opening sessions.
  • With the default authenticator, HTTP callers are anonymous in practice (see Who is the HTTP caller?).
  • webda mcp over stdio trusts whoever launches it: that process acts with the permissions of --user (or anonymous permissions without it).
  • With McpOAuthAuthenticator, callers present an OAuth bearer token whose scope limits the tools they see and call (see OAuth).

Reference​

  • Source: packages/mcp
  • Design: docs/superpowers/specs/2026-10-03-webda-mcp-design.md