Class: Parse::Agent::MCPServer

Inherits:
Object
  • Object
show all
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.

Examples:

Start the server

Parse::Agent.enable_mcp!
Parse::Agent::MCPServer.run(port: 3001)

With custom configuration

server = Parse::Agent::MCPServer.new(
  port: 3001,
  permissions: :readonly,
  session_token: nil
)
server.start

See Also:

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.

Raises:

  • (ArgumentError)

    if rate_limiter is provided but does not respond to :check!

%w[127.0.0.1 ::1 localhost].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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 = 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_portObject

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

#agentParse::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.

Returns:

  • (Parse::Agent)

    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

#hostString (readonly)

Returns the host to bind to.

Returns:

  • (String)

    the host to bind to



90
91
92
# File 'lib/parse/agent/mcp_server.rb', line 90

def host
  @host
end

#portInteger (readonly)

Returns the port number.

Returns:

  • (Integer)

    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)

Parameters:

  • port (Integer) (defaults to: nil)

    port to listen on

  • permissions (Symbol) (defaults to: :readonly)

    agent permission level

  • session_token (String, nil) (defaults to: nil)

    optional session token

  • host (String) (defaults to: "127.0.0.1")

    host to bind to

  • rate_limiter (#check!, nil) (defaults to: nil)

    optional external rate limiter



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: permissions,
    session_token: session_token,
    host: host,
    api_key: api_key,
    rate_limiter: rate_limiter,
  )
  server.start
end

Instance Method Details

#startObject

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.permissions}"
  puts "Tools available: #{@agent.allowed_tools.join(", ")}"

  @server.start
end

#stopObject

Stop the server



195
196
197
# File 'lib/parse/agent/mcp_server.rb', line 195

def stop
  @server&.shutdown
end