Skip to main content

MCP Virtual Server exposition

Otoroshi can expose tool functions and MCP connectors as an MCP server that external MCP clients connect to.

The way to go is to define an MCP Virtual Server and expose it. A virtual server is a single, reusable, persisted definition that carries the whole exposition complexity in one place: which tool functions / MCP connectors to serve, plus filtering, OAuth, RBAC (scope β†’ tool), audience binding, meta mode, rate limiting & caching, managed resources/prompts and overlays. The exposition plugin then simply references it with server_ref β€” so your access control and settings live on the virtual server, not scattered across routes.

You can set the same settings inline on a plugin, or override individual fields on top of a referenced virtual server, but for anything beyond a quick test, expose a virtual server.

The recommended transport is Streamable HTTP; the other transports still exist but are deprecated or experimental.

Transports​

TransportPluginStatus
Streamable HTTPMcpRespEndpointβœ… Recommended β€” the current MCP HTTP transport (POST + SSE). What MCP clients and MCP apps use.
SSEMcpSseEndpoint⚠️ Deprecated β€” the legacy HTTP+SSE transport, kept for backward compatibility.
WebSocketMcpWebsocketEndpointπŸ§ͺ Experimental β€” not part of the MCP standard; use only if you specifically need it.

Unless you have a specific reason, expose your MCP servers over Streamable HTTP.

Supported MCP methods​

All exposition plugins implement the full MCP protocol:

MethodDescription
initializeInitialize the MCP session
tools/listList all available tools (from tool functions and MCP connectors)
tools/callCall a tool by name with arguments
resources/listList all available resources from connected MCP servers
resources/readRead a resource by its URI. Returns text or binary content.
resources/templates/listList all available resource templates from connected MCP servers
prompts/listList all available prompts with their arguments
prompts/getGet a prompt by name with arguments. Returns the prompt description and messages (each with role and content).

Tools​

When listing tools, each tool includes at least:

  • name - The tool name
  • description - A description of the tool
  • inputSchema - The JSON schema of the tool arguments (type, properties, required)

Tools coming from a tool function entity can also carry extra MCP metadata when configured on the entity (see tool function entity):

  • outputSchema - JSON schema describing the tool result (MCP outputSchema)
  • annotations - MCP tool annotations (e.g. readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title)
  • _meta - Free-form metadata object, used by MCP apps and hybrid-auth scenarios

These fields are only emitted when set on the tool function; they are omitted (not null) otherwise.

For tools coming from an MCP connector, the fidelity depends on the connector transport:

  • connectors using the Streamable HTTP transport (transport.kind: "http") are proxied with a raw, full-fidelity passthrough: the upstream tool definitions are forwarded as-is, preserving outputSchema, annotations, _meta, title and rich tools/call results (structuredContent, resource links, images, ...). This is what MCP apps need.
  • for every other connector transport, tool definitions go through the langchain4j abstraction: _meta, annotations and title are recovered and re-emitted, but outputSchema is not available on that path.

Resources​

When listing resources, each resource includes:

  • uri - The resource URI
  • name - The resource name
  • description - A description of the resource
  • mimeType - The MIME type of the resource

When reading a resource, the content is returned as either text (with uri, mimeType, text) or binary blob (with uri, mimeType, blob).

Resource Templates​

When listing resource templates, each template includes:

  • uriTemplate - The URI template with parameters
  • name - The template name
  • description - A description of the template
  • mimeType - The MIME type of resources generated by this template

Prompts​

When listing prompts, each prompt includes:

  • name - The prompt name
  • description - A description of the prompt
  • arguments - An array of arguments, each with name, description, and required

When getting a prompt, the response includes:

  • description - The prompt description
  • messages - An array of messages, each with role (e.g. user, assistant) and content (text or image)

The McpRespEndpoint plugin exposes your tools and connectors over the standard MCP Streamable HTTP transport (a single endpoint handling POST requests and SSE streams). This is the transport MCP clients and MCP apps expect β€” use it by default.

Point it at an MCP Virtual Server with server_ref (the recommended way β€” the virtual server carries all the config and access control), or set the same configuration inline.

Configuration​

{
"_loc": {
"tenant": "default",
"teams": [
"default"
]
},
"id": "route_2629d12c2-130b-42d5-aba5-b584b95961c9",
"name": "MCP HTTP server exposition",
"description": "MCP HTTP server exposition",
"tags": [],
"metadata": {},
"enabled": true,
"debug_flow": false,
"export_reporting": false,
"capture": false,
"groups": [
"default"
],
"bound_listeners": [],
"frontend": {
"domains": [
"mcp-http-expo.oto.tools"
],
"strip_path": true,
"exact": false,
"headers": {},
"query": {},
"methods": []
},
"backend": {
"targets": [
{
"id": "target_1",
"hostname": "request.otoroshi.io",
"port": 443,
"tls": true,
"weight": 1,
"predicate": {
"type": "AlwaysMatch"
},
"protocol": "HTTP/1.1",
"ip_address": null,
"tls_config": {
"certs": [],
"trusted_certs": [],
"enabled": false,
"loose": false,
"trust_all": false
}
}
],
"root": "/",
"rewrite": false,
"load_balancing": {
"type": "RoundRobin"
}
},
"backend_ref": null,
"plugins": [
{
"enabled": true,
"debug": false,
"plugin": "cp:otoroshi.next.plugins.OverrideHost",
"include": [],
"exclude": [],
"config": {},
"bound_listeners": [],
"plugin_index": {
"transform_request": 0
},
"nodeId": "cp:otoroshi.next.plugins.OverrideHost"
},
{
"enabled": true,
"debug": false,
"plugin": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.aigateway.plugins.McpRespEndpoint",
"include": [],
"exclude": [],
"config": {
"name": "first-mcp-http-server",
"version": "1.0.0",
"refs": [
"tool-function_0511ff44-0ce5-487d-aa1c-d1980ac9a4b8"
],
"mcp_refs": []
},
"bound_listeners": [],
"plugin_index": {},
"nodeId": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.extensions.aigateway.plugins.McpRespEndpoint"
}
],
"kind": "proxy.otoroshi.io/Route"
}

Protected MCP Streamable HTTP preset​

ProtectedMcpStreamableHttpPreset is an Otoroshi preset plugin: from a single MCP Virtual Server reference, it wires onto one route both halves of a production-ready protected MCP server:

  1. the MCP Streamable HTTP endpoint (McpRespEndpoint) on a customizable path (default /mcp), and
  2. the Protected Resource Metadata document (RFC 9728) on the standard well-known path.

The virtual server carries all the settings (OAuth enforcement, auth module, audience binding, scopes, meta mode, rate limits…), so both child plugins are configured from just the server ref.

ParameterTypeDefaultDescription
server_refstringβ€”The MCP Virtual Server that drives both plugins.
mcp_pathstring/mcpPath of the MCP Streamable HTTP endpoint.
well_known_pathstring/.well-known/oauth-protected-resourcePath of the Protected Resource Metadata document.
{
"server_ref": "mcp-virtual-server_xxxxx",
"mcp_path": "/mcp",
"well_known_path": "/.well-known/oauth-protected-resource"
}

Other transports​

These plugins expose the same surface and the same configuration as the Streamable HTTP plugin, but over a different transport. Prefer Streamable HTTP unless you have a specific reason.

SSE β€” McpSseEndpoint (deprecated)​

The legacy HTTP+SSE transport (a GET SSE stream plus POSTed requests). Kept for backward compatibility with older clients. Deprecated β€” migrate to Streamable HTTP.

WebSocket β€” McpWebsocketEndpoint (experimental)​

Exposes the MCP JSON-RPC surface over a WebSocket. Experimental and not part of the MCP standard.

Local stdio proxy β€” McpLocalProxyEndpoint (non-official)​

The MCP Tools Endpoint lets MCP clients that only speak the local stdio transport (for example desktop AI apps) reach an Otoroshi MCP route through the local proxy npx @cloud-apim/otoroshi-mcp-proxy: the proxy runs locally and bridges stdio ↔ the Otoroshi MCP route. It uses the same configuration (including server_ref) as the other exposition plugins.

Audit events​

All exposition plugins can emit audit events for every MCP method call. This is useful for monitoring, debugging, and auditing MCP usage across your infrastructure.

Two complementary audit streams

McpAudit documented here covers the server/exposition side β€” calls that external clients make to the MCP servers Otoroshi exposes. The client side β€” calls Otoroshi makes to upstream MCP servers through an MCP connector β€” is covered by a separate McpClientAudit event.

Real-time metrics (always on)

Independently of these (opt-in) audit events, every served MCP method also feeds Otoroshi's real-time metrics: mcp.server.calls, mcp.server.errors, and per-method mcp.server.<method>.calls / .errors / .duration. These are emitted whenever Otoroshi metrics are enabled β€” no flag required. See Observability β€” Real-time MCP metrics.

Enabling audit events​

Set emit_audit_events to true in the configuration:

{
"name": "my-mcp-server",
"version": "1.0.0",
"refs": ["tool-function_xxxxx"],
"mcp_refs": ["mcp-connector_xxxxx"],
"emit_audit_events": true
}

By default, this option is disabled (false).

Audit event payload​

Each MCP method call will produce an AuditEvent with the audit type McpAudit. The event contains the following fields:

FieldTypeDescription
request_idstring | nullThe Otoroshi request id (snowflake) of the current request, for correlation with LLMUsageAudit and McpClientAudit events
mcp_methodstringThe MCP JSON-RPC method called (e.g. tools/call, resources/list, initialize)
mcp_idnumberThe JSON-RPC request id
mcp_request_payloadobjectThe full JSON-RPC request payload
mcp_responseobjectThe JSON-RPC response envelope (jsonrpc, id, result). Captured for all transports. null only on failures that produce no response.
transportstringThe transport protocol used: http, sse, or websocket
durationnumberThe request duration in milliseconds
statusstringsuccess or error
errorstringError message if the request failed, null otherwise
userobjectThe authenticated user information (if available)
apikeyobjectThe API key used for the request (if available)
routeobjectThe Otoroshi route that handled the request

Example audit event​

{
"@type": "AuditEvent",
"audit": "McpAudit",
"request_id": "1905616593920983819",
"mcp_method": "tools/call",
"mcp_id": 3,
"mcp_request_payload": {
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "Paris" }
}
},
"mcp_response": {
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "It is 21Β°C and sunny in Paris." }
]
}
},
"transport": "http",
"duration": 142,
"status": "success",
"error": null,
"user": null,
"apikey": { "clientId": "xxx", "clientName": "my-api-key" },
"route": { "id": "route_xxx", "name": "mcp-server" }
}

Filtering audit events in data exporters​

You can use Otoroshi's data exporter filtering to collect only MCP audit events:

{
"include": [
{
"@type": "AuditEvent",
"audit": "McpAudit"
}
]
}

Zero-Trust alerts​

When a zero-trust control fires (an anti-rug-pull mutation, a guardrail denial on a tool description or result, or a redaction), the server emits a separate McpZeroTrustAlert audit event β€” correlated with McpAudit by request_id β€” carrying zerotrust_kind (rugpull | guardrail | redaction), mcp_tool, blocked (monitor vs enforce), and a detail object. These run alongside the real-time metrics mcp.zerotrust.<kind>.alerts and mcp.zerotrust.<kind>.blocks, so you can monitor zero-trust activity before turning on enforcement.