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.
1. Create the inbound service
Section titled “1. Create the inbound service”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}/servicesRecord 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.
2. Enable serving and bind the catalog
Section titled “2. Enable serving and bind the catalog”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.
3. Reach the endpoint
Section titled “3. Reach the endpoint”MCP dispatches on the inbound service path and, for a published application, on the application host:
POST /api/v1/services/{endpoint-service-uuid}/inboundPOST https://<slug>.merofoundry.app/mcpBoth 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:
initializereturns the negotiated protocol version and anMcp-Session-Idresponse header. Send that header on every later call.notifications/initialized, a notification; expect202.tools/listreturns the enabled catalog: each tool’sname,description, andinputSchema.tools/callwith{"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.
4. Authenticate with consumer OAuth
Section titled “4. Authenticate with consumer OAuth”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 theresourceof both protected-resource documents below and the audience consent issues tokens for. The retiredconsumer_oauth_audienceapplication setting is ignored and can no longer be set. - Its grant is still active, and its application user still exists with the same
token_versionit had at consent. Raising the user’stoken_versionrevokes every token issued for them. - It carries the scope for the method:
mcp:tools:listfortools/list,mcp:tools:callfortools/call, and alsomcp:streamwhen the called tool streams.
The 401 login challenge
Section titled “The 401 login challenge”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 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://<slug>.merofoundry.app/.well-known/oauth-protected-resource"
HTTP/1.1 401 UnauthorizedWWW-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.
5. Anonymous tools: two opt-ins
Section titled “5. Anonymous tools: two opt-ins”Two switches control access without a credential, and both are off by default. A tool is served anonymously only when both are on:
- The endpoint:
mcpSettings.allow_anonymousset totrue. - The tool: a boolean field on the tool catalog model, mapped as
anonymousintool_field_mappings, andtrueon 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:
initializeand sendnotifications/initialized;tools/list, which returns only enabled tools flagged anonymous;tools/calla flagged tool. It runs as the anonymous service actor with nocurrent_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.
The warning when you open the endpoint
Section titled “The warning when you open the endpoint”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.
