Module: Otto::MCP

Defined in:
lib/otto/mcp/endpoint.rb,
lib/otto/mcp/core.rb,
lib/otto/mcp/errors.rb,
lib/otto/mcp/server.rb,
lib/otto/mcp/options.rb,
lib/otto/mcp/protocol.rb,
lib/otto/mcp/registry.rb,
lib/otto/mcp/auth/token.rb,
lib/otto/mcp/route_parser.rb,
lib/otto/mcp/rate_limiting.rb,
lib/otto/mcp/schema_validation.rb

Overview

Model Context Protocol support.

Defined Under Namespace

Modules: Auth, Core, Options Classes: InternalHandler, Protocol, RateLimitMiddleware, RateLimiter, Registry, RouteParser, SchemaValidationMiddleware, Server, ToolNotFoundError, ValidationError, Validator

Class Method Summary collapse

Class Method Details

.endpoint_path?(path, endpoint) ⇒ Boolean

Whether path is the MCP endpoint endpoint: the one request the router dispatches to the MCP handler, and nothing else.

The MCP route is a literal route. The router normalizes PATH_INFO with Otto::Utils.normalize_path and looks it up by exact equality, so it never dispatches a sibling path to the MCP handler. The throttles, the throttled responder, the log subscriber, the token middleware and the schema validator all used to classify by prefix (start_with?) instead: with MCP mounted at /a, every /admin request was counted by 'mcp_requests:/a' and could be answered with an MCP JSON-RPC 429, or a 401, for a path the MCP handler can never receive; with MCP at / that was every route in the app.

Both sides go through the router's normalize_path, as LocalhostGuard does for the Caddy endpoint, so a trailing slash or percent-encoding on either side cannot make these guards and the router disagree about a request.

Callers must pass PATH_INFO (Rack::Request#path_info), never Rack::Request#path, which prepends SCRIPT_NAME. The router matches on PATH_INFO alone, so when the host app mounts Otto under a prefix (map '/api' { run otto }) the endpoint /_mcp is dispatched for PATH_INFO=/_mcp while #path reads /api/_mcp and never matches.

Parameters:

  • path (String, nil)

    raw PATH_INFO of the request

  • endpoint (String, nil)

    configured MCP endpoint

Returns:

  • (Boolean)


35
36
37
38
39
# File 'lib/otto/mcp/endpoint.rb', line 35

def self.endpoint_path?(path, endpoint)
  return false if endpoint.nil?

  Otto::Utils.normalize_path(path) == Otto::Utils.normalize_path(endpoint)
end