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 |
# 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. @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, ) 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)
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/parse/agent/mcp_server.rb', line 174 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
195 196 197 |
# File 'lib/parse/agent/mcp_server.rb', line 195 def stop @server&.shutdown end |