Skip to content

Serve an application MCP endpoint

Serving MCP is off by default and never implicit. An application exposes an MCP endpoint only when two explicit choices are made: an inbound service to receive JSON-RPC, and mcpSettings.enabled set to true. A tool catalog on its own serves nothing.

Every caller is an application user. The endpoint accepts one credential, the consumer OAuth access token an MCP client obtains by signing in as an application user and approving the client. Anonymous access is a separate, opt-in decision covered in section 5.

Create a webhook_in service in the same tenant, workspace, and application as the catalog. Its id is the endpoint MCP dispatches on.

POST /api/v1/public/applications/{application_id}/services

Record the returned service id. The service’s own auth type and its public setting apply only to ordinary webhook callers of that service. They have no effect on MCP: the service’s API key does not open the MCP endpoint, and a public service does not make it anonymous. A webhook_in service can keep an API key for its webhook callers while serving MCP to application users.

Set the application’s mcpSettings to turn serving on and point it at the inbound service and the tool catalog. enabled must be true; leave it unset or false and the endpoint returns not-found.

{
"mcpSettings": {
"enabled": true,
"endpoint_service_id": "<inbound-service-uuid>",
"tool_model_id": "<mcp-tool-model-uuid>",
"tool_field_mappings": {
"name": "<name-field-uuid>",
"description": "<description-field-uuid>",
"input_schema": "<input-schema-field-uuid>",
"output_schema": "<output-schema-field-uuid>",
"streaming": "<streaming-field-uuid>",
"enabled": "<enabled-field-uuid>"
}
}
}

The catalog model and its field-id mappings are the tool-catalog recipe’s subject. The six mappings above are required. Only catalog records whose enabled field is true are served.

Two optional endpoint settings sit beside them:

Setting Type Default Meaning
allow_anonymous boolean false Opens the endpoint to callers without a credential (section 5).
login_path path /login The application’s sign-in page. It must be a same-origin absolute path: one leading /, with no scheme, host, //, backslash, query, fragment or whitespace.

Any other value is rejected when the settings are saved.

MCP dispatches on the inbound service path and, for a published application, on the application host:

POST /api/v1/services/{endpoint-service-uuid}/inbound
POST https://<slug>.merofoundry.app/mcp

Both paths run the same dispatcher and apply the same authentication. Speak JSON-RPC 2.0 over StreamableHTTP, sending Authorization: Bearer <consumer access token> on every request:

  1. initialize returns the negotiated protocol version and an Mcp-Session-Id response header. Send that header on every later call.
  2. notifications/initialized, a notification; expect 202.
  3. tools/list returns the enabled catalog: each tool’s name, description, and inputSchema.
  4. tools/call with {"name": "<tool>", "arguments": {...}} runs the tool through the application’s rule pipeline.

What a tool does, and which rows it may touch, is whatever the application’s rules and saved queries enforce; a tool with no bound rule returns an empty result. An authenticated call runs as the signed-in application user: the rule event’s actor is that user’s identity record, and require_query binds current_user_id to it.

An application whose mcpSettings.enabled is not true, or a host that does not resolve to a published application, answers 404 on both paths. Repeated initialize calls from one address or to one application are rate limited (429) before any credential is checked.

The endpoint accepts only a consumer OAuth access token: one issued for an application user of this application, through the application’s consumer consent, for this endpoint. These are refused: the endpoint service’s API key, a platform OAuth token, an application API key, a consumer session token sent as a bearer, and the consumer session cookie (the endpoint never reads cookies).

A token is accepted only when all of these hold:

  • Its audience is this endpoint’s resource, the inbound URL of the service in endpoint_service_id: https://<platform>/api/v1/services/{endpoint-service-uuid}/inbound. A token issued for another service of the same application is refused. The same value is the resource of both protected-resource documents below and the audience consent issues tokens for. The retired consumer_oauth_audience application setting is ignored and can no longer be set.
  • Its grant is still active, and its application user still exists with the same token_version it had at consent. Raising the user’s token_version revokes every token issued for them.
  • It carries the scope for the method: mcp:tools:list for tools/list, mcp:tools:call for tools/call, and also mcp:stream when the called tool streams.

A request with no credential, or with a credential that fails any check, gets 401 Unauthorized and never runs a tool. The WWW-Authenticate header names the protected-resource document for the path the client used (RFC 9728), and, for a refused token, the OAuth error:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://<slug>.merofoundry.app/.well-known/oauth-protected-resource"
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_scope", error_description="OAuth principal does not include the required permission.", scope="mcp:tools:call", resource_metadata="https://<platform>/.well-known/oauth-protected-resource/app/{application_id}"

The application host’s /mcp links its own bare /.well-known/oauth-protected-resource; the inbound service path links the platform’s /.well-known/oauth-protected-resource/app/{application_id}. error is invalid_token for an unknown, expired, revoked, wrong-audience or non-consumer token, and insufficient_scope for a missing scope. With no credential there is no error. An MCP client reads the document, registers with the application’s authorization server, and sends the user to sign in.

login_path names the application’s sign-in page for that step. A signed-out user is sent there with returnTo set to the page they came from, a same-origin path. After signing in, the sign-in page sends them back to returnTo. A bring-your-own front end must honour the same returnTo contract on its own sign-in page.

Two switches control access without a credential, and both are off by default. A tool is served anonymously only when both are on:

  1. The endpoint: mcpSettings.allow_anonymous set to true.
  2. The tool: a boolean field on the tool catalog model, mapped as anonymous in tool_field_mappings, and true on that tool’s record.
{
"mcpSettings": {
"enabled": true,
"allow_anonymous": true,
"endpoint_service_id": "<inbound-service-uuid>",
"tool_model_id": "<mcp-tool-model-uuid>",
"tool_field_mappings": {
"name": "<name-field-uuid>",
"description": "<description-field-uuid>",
"input_schema": "<input-schema-field-uuid>",
"output_schema": "<output-schema-field-uuid>",
"streaming": "<streaming-field-uuid>",
"enabled": "<enabled-field-uuid>",
"anonymous": "<anonymous-field-uuid>"
}
}
}

The anonymous mapping is optional. Without it no tool is anonymous. Only a stored boolean true flags a tool. A missing value, false, null, or a string such as "true" keeps the tool private. Turning on allow_anonymous alone exposes no tools.

When allow_anonymous is false, every method without a credential, initialize included, gets the 401 challenge. When it is true, a caller without a credential can:

  • initialize and send notifications/initialized;
  • tools/list, which returns only enabled tools flagged anonymous;
  • tools/call a flagged tool. It runs as the anonymous service actor with no current_user_id, so tools scoped to the caller’s own rows return nothing.

Any other anonymous call, including tools/call of an unflagged tool, gets the 401 challenge, so the client can sign in and retry in the same session. A request that does present a credential is always authenticated: an invalid token is refused with 401 even for an anonymous tool. A signed-in user sees and may call every enabled tool their scopes allow.

Turning allow_anonymous from off to on succeeds, but the response carries a warning. This applies to the application update API (PATCH /api/v1/public/applications/{application_id} or the console’s application settings) and to the update_application MCP tool:

{
"warnings": [
"This endpoint now serves tools to unauthenticated callers; tools currently flagged anonymous: lookup_hours, search_catalog."
]
}

The sentence is fixed. {tools} is the sorted, comma-separated list of enabled tools flagged anonymous at that moment, or (none) when there are none. The same change writes an application.mcp_anonymous_access_enabled audit event recording who made it and that tool list. Saving allow_anonymous: true again, or turning it off, produces no warning and no event. Turning it on again later warns again.

Review the warning. Every tool it names can now be called by anyone who can reach the endpoint.