Class: Parse::Agent::MCPServer
- Inherits:
-
Object
- Object
- Parse::Agent::MCPServer
- Defined in:
- lib/parse/agent/mcp_server.rb
Overview
MCP (Model Context Protocol) HTTP Server for Parse Stack. Enables external AI agents (Claude, LM Studio, etc.) to interact with Parse data over HTTP using the MCP protocol specification.
Since the Rack refactor this class is a thin WEBrick wrapper around MCPRackApp. Embedded deployments (Sinatra, Rails) should mount MCPRackApp directly with their own agent factory; this class remains for standalone server deployments and back-compat.
Constant Summary collapse
- PROTOCOL_VERSION =
MCP Protocol version
MCPDispatcher::PROTOCOL_VERSION
- CAPABILITIES =
Server capabilities
MCPDispatcher::CAPABILITIES
- MAX_BODY_SIZE =
Maximum allowed request body size (1 MB) — kept as a back-compat constant.
MCPRackApp::DEFAULT_MAX_BODY_SIZE
- MAX_JSON_NESTING =
Maximum JSON nesting depth — kept as a back-compat constant.
MCPRackApp::MAX_JSON_NESTING
- MCP_API_KEY_HEADER =
HTTP header for MCP API key authentication
"X-MCP-API-Key"- LOOPBACK_HOSTS =
Create a new MCP server instance
Loopback hosts that are safe to bind to without an API key.
%w[127.0.0.1 ::1 localhost].freeze
Class Attribute Summary collapse
-
.default_port ⇒ Object
Returns the value of attribute default_port.
Instance Attribute Summary collapse
-
#agent ⇒ Parse::Agent
readonly
The template agent used by the /tools listing endpoint and as a settings source for per-request agents.
-
#host ⇒ String
readonly
The host to bind to.
-
#port ⇒ Integer
readonly
The port number.
Class Method Summary collapse
-
.run(port: nil, permissions: :readonly, session_token: nil, host: "127.0.0.1", api_key: nil, rate_limiter: nil) ⇒ Object
Start the MCP server (blocking).
Instance Method Summary collapse
-
#initialize(port: 3001, host: "127.0.0.1", permissions: :readonly, session_token: nil, api_key: nil, rate_limiter: nil, pre_auth_rate_limiter: nil, allowed_origins: nil, require_custom_header: nil) ⇒ MCPServer
constructor
A new instance of MCPServer.
-
#start ⇒ Object
Start the HTTP server (blocking).
-
#stop ⇒ Object
Stop the server.
Constructor Details
#initialize(port: 3001, host: "127.0.0.1", permissions: :readonly, session_token: nil, api_key: nil, rate_limiter: nil, pre_auth_rate_limiter: nil, allowed_origins: nil, require_custom_header: nil) ⇒ MCPServer
Returns a new instance of MCPServer.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/parse/agent/mcp_server.rb', line 112 def initialize(port: 3001, host: "127.0.0.1", permissions: :readonly, session_token: nil, api_key: nil, rate_limiter: nil, pre_auth_rate_limiter: nil, allowed_origins: nil, require_custom_header: nil) if rate_limiter && !rate_limiter.respond_to?(:check!) raise ArgumentError, "rate_limiter must respond to #check!" end if pre_auth_rate_limiter && !pre_auth_rate_limiter.respond_to?(:check!) raise ArgumentError, "pre_auth_rate_limiter must respond to #check!" end effective_api_key = api_key || ENV["MCP_API_KEY"] # NEW-MCP-1: a non-loopback bind without an API key is an unauthenticated # network-exposed JSON-RPC endpoint. Refuse to start. Operators who # genuinely want this — e.g., behind a reverse proxy that handles # auth — should bind to localhost and let the proxy forward, or # set MCP_API_KEY explicitly even when "the proxy authenticates" # (defense in depth). if !LOOPBACK_HOSTS.include?(host.to_s) && effective_api_key.to_s.empty? raise ArgumentError, "MCPServer refuses to bind non-loopback host #{host.inspect} without an api_key. " \ "Set MCP_API_KEY in the environment, pass api_key: explicitly, or use a loopback " \ "host (one of: #{LOOPBACK_HOSTS.join(', ')})." end @port = port @host = host @api_key = effective_api_key @permissions = @session_token = session_token # Shared limiter across requests so per-request agents (built in # agent_factory) don't reset their window on every call. The # rate-limit budget is a server-level resource, not a per-Agent one. @shared_rate_limiter = rate_limiter || RateLimiter.new # Template agent for the /tools listing endpoint and for inspection # via #agent. NOT used for live request dispatch — see agent_factory. @agent = Parse::Agent.new( permissions: @permissions, session_token: @session_token, rate_limiter: @shared_rate_limiter, ) @server = nil # The Rack app does the heavy lifting. Its agent_factory enforces the # API key and constructs a FRESH Parse::Agent per request so the # per-instance state (@conversation_history, @operation_log, token # counters) cannot leak between requests. # pre_auth_rate_limiter: closes NEW-MCP-6 — runs before the factory # is invoked so an empty or malformed body can't amplify into a # Parse Server round-trip. # NEW-9: on an unauthenticated loopback dev bind with no explicit CSRF # gate configured, enable a loopback-only Origin policy by default to # mitigate browser DNS-rebinding (a malicious page resolving a hostname # to 127.0.0.1 and POSTing to the agent). The attacker page always # carries a non-loopback Origin and is refused; native (no-Origin) # clients and real local browser UIs are unaffected. Skipped when an # API key is set (auth already gates) or the operator configured the # Origin/custom-header gates themselves. loopback_csrf_default = LOOPBACK_HOSTS.include?(host.to_s) && @api_key.to_s.empty? && allowed_origins.nil? && require_custom_header.nil? if loopback_csrf_default warn "[Parse::Agent::MCPServer] Binding #{host}:#{port} without an API key. " \ "Enabling a loopback-only Origin policy to mitigate browser DNS-rebinding. " \ "For anything beyond local single-user dev set MCP_API_KEY (or pass api_key:), " \ "and/or configure allowed_origins:/require_custom_header:." end @rack_app = MCPRackApp.new( agent_factory: method(:agent_factory), pre_auth_rate_limiter: pre_auth_rate_limiter, allowed_origins: allowed_origins, require_custom_header: require_custom_header, loopback_csrf_default: loopback_csrf_default, ) end |
Class Attribute Details
.default_port ⇒ Object
Returns the value of attribute default_port.
60 61 62 |
# File 'lib/parse/agent/mcp_server.rb', line 60 def default_port @default_port end |
Instance Attribute Details
#agent ⇒ Parse::Agent (readonly)
Returns the template agent used by the /tools listing endpoint and as a settings source for per-request agents. Hot tools in MCP requests run against fresh per-request instances; do NOT share this object across threads for mutable state inspection.
96 97 98 |
# File 'lib/parse/agent/mcp_server.rb', line 96 def agent @agent end |
#host ⇒ String (readonly)
Returns the host to bind to.
90 91 92 |
# File 'lib/parse/agent/mcp_server.rb', line 90 def host @host end |
#port ⇒ Integer (readonly)
Returns the port number.
87 88 89 |
# File 'lib/parse/agent/mcp_server.rb', line 87 def port @port end |
Class Method Details
.run(port: nil, permissions: :readonly, session_token: nil, host: "127.0.0.1", api_key: nil, rate_limiter: nil) ⇒ Object
Start the MCP server (blocking)
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/parse/agent/mcp_server.rb', line 69 def run(port: nil, permissions: :readonly, session_token: nil, host: "127.0.0.1", api_key: nil, rate_limiter: nil) unless Parse::Agent.mcp_enabled? raise "MCP server not enabled. Call Parse::Agent.enable_mcp! first" end server = new( port: port || @default_port, permissions: , session_token: session_token, host: host, api_key: api_key, rate_limiter: rate_limiter, ) server.start end |
Instance Method Details
#start ⇒ Object
Start the HTTP server (blocking)
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/parse/agent/mcp_server.rb', line 193 def start @server = WEBrick::HTTPServer.new( Port: @port, BindAddress: @host, Logger: WEBrick::Log.new($stdout, WEBrick::Log::INFO), AccessLog: [[::File.open(::File::NULL, "w"), ""]], # Suppress access log ) setup_routes trap("INT") { stop } trap("TERM") { stop } puts "Parse MCP Server starting on http://#{@host}:#{@port}" puts "Permissions: #{@agent.}" puts "Tools available: #{@agent.allowed_tools.join(", ")}" @server.start end |
#stop ⇒ Object
Stop the server
214 215 216 |
# File 'lib/parse/agent/mcp_server.rb', line 214 def stop @server&.shutdown end |