Class: MCPClient::ServerBase
- Inherits:
-
Object
- Object
- MCPClient::ServerBase
- Defined in:
- lib/mcp_client/server_base.rb
Overview
Base class for MCP servers - serves as the interface for different server implementations
Direct Known Subclasses
Constant Summary collapse
- RELATED_TASK_META_KEY =
Get server capabilities MCP 2025-11-25 tasks: all messages related to a task MUST carry the io.modelcontextprotocol/related-task key in _meta. Reserved key name:
'io.modelcontextprotocol/related-task'- MAX_LIST_PAGES =
Safety bound on the number of pages followed when auto-paginating a cursor-based list operation, to protect against a server that returns a nextCursor indefinitely.
1000
Instance Attribute Summary collapse
-
#instructions ⇒ String?
readonly
Server-declared instructions from the initialize result, if any.
-
#logger ⇒ Logger
readonly
The transport logger.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#read_timeout ⇒ Object
readonly
Returns the value of attribute read_timeout.
Instance Method Summary collapse
-
#call_tool(tool_name, parameters) ⇒ Object
Call a tool with the given parameters.
-
#call_tool_streaming(tool_name, parameters) ⇒ Enumerator
Stream a tool call result (default implementation returns single-value stream).
-
#cancel_subscription(subscription) ⇒ void
Cancel a subscription opened with #listen.
-
#capabilities ⇒ Hash?
Server capabilities.
-
#capability?(*path) ⇒ Boolean
Whether the server declared the given (possibly nested) capability during initialization.
-
#cleanup ⇒ Object
Clean up the server connection.
-
#client_info=(info) ⇒ Object
Host-supplied Implementation info sent as clientInfo during initialize (MCP 2025-11-25 Implementation: name, version, plus optional title, description, websiteUrl, icons).
-
#connect ⇒ Boolean
Initialize a connection to the MCP server.
-
#discovery_refresh_needed? ⇒ Boolean
Whether the DiscoverResult behind the negotiated capabilities may still answer for the request about to go out.
-
#get_prompt(prompt_name, parameters) ⇒ Object
Get a prompt with the given parameters.
-
#initialize(name: nil) ⇒ ServerBase
constructor
Initialize the server with a name.
-
#list_prompts ⇒ Array<MCPClient::Prompt>
List all prompts available from the MCP server.
-
#list_resource_templates(cursor: nil) ⇒ Hash
List all resource templates available from the MCP server.
-
#list_resources(cursor: nil) ⇒ Hash
List all resources available from the MCP server.
-
#list_tools ⇒ Array<MCPClient::Tool>
List all tools available from the MCP server.
-
#listen(notifications:, ack_timeout: nil) {|method, params| ... } ⇒ MCPClient::Subscription
Open a long-lived notification stream (MCP 2026-07-28 subscriptions/listen).
-
#merge_related_task_meta(result, params) ⇒ Hash
Echo the related-task _meta of an incoming server request onto the outgoing result, so responses to task-related requests (elicitation or sampling during input_required) stay associated with their task.
-
#on_cache_invalidation {|method, params| ... } ⇒ void
Register a callback for the caches a notification invalidates, run before the notification is delivered to a subscription's listeners.
-
#on_notification {|method, params| ... } ⇒ void
Register a callback to receive JSON-RPC notifications.
-
#ping ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call).
-
#read_resource(uri) ⇒ Array<MCPClient::ResourceContent>
Read a resource by its URI.
-
#require_capability!(*path, method:) ⇒ Object
Raise unless the server negotiated the given capability (MCP lifecycle: "Only use capabilities that were successfully negotiated").
-
#resource_not_found_error(uri, error) ⇒ MCPClient::Errors::ResourceNotFound
Map a resources/read error response to ResourceNotFound.
-
#resource_not_found_response?(error) ⇒ Boolean
Whether a resources/read error response means the resource does not exist, given this session's protocol era.
-
#rpc_notify(method, params = {}) ⇒ void
Send a JSON-RPC notification (no response expected).
-
#rpc_request(method, params = {}) ⇒ Object
Send a JSON-RPC request and return the result.
-
#session_epoch ⇒ Integer
How many times this transport's session ended (cleanup, a restarted stdio process, a reconnect).
-
#subscribe_resource(uri) ⇒ Boolean
Subscribe to resource updates.
-
#unsubscribe_resource(uri) ⇒ Boolean
Unsubscribe from resource updates.
Constructor Details
#initialize(name: nil) ⇒ ServerBase
Initialize the server with a name
34 35 36 |
# File 'lib/mcp_client/server_base.rb', line 34 def initialize(name: nil) @name = name end |
Instance Attribute Details
#instructions ⇒ String? (readonly)
Server-declared instructions from the initialize result, if any
18 19 20 |
# File 'lib/mcp_client/server_base.rb', line 18 def instructions @instructions end |
#logger ⇒ Logger (readonly)
Returns the transport logger.
273 274 275 |
# File 'lib/mcp_client/server_base.rb', line 273 def logger @logger end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
10 11 12 |
# File 'lib/mcp_client/server_base.rb', line 10 def name @name end |
#read_timeout ⇒ Object (readonly)
Returns the value of attribute read_timeout.
14 15 16 |
# File 'lib/mcp_client/server_base.rb', line 14 def read_timeout @read_timeout end |
Instance Method Details
#call_tool(tool_name, parameters) ⇒ Object
Call a tool with the given parameters
54 55 56 |
# File 'lib/mcp_client/server_base.rb', line 54 def call_tool(tool_name, parameters) raise NotImplementedError, 'Subclasses must implement call_tool' end |
#call_tool_streaming(tool_name, parameters) ⇒ Enumerator
Stream a tool call result (default implementation returns single-value stream)
244 245 246 247 248 |
# File 'lib/mcp_client/server_base.rb', line 244 def call_tool_streaming(tool_name, parameters) Enumerator.new do |yielder| yielder << call_tool(tool_name, parameters) end end |
#cancel_subscription(subscription) ⇒ void
This method returns an undefined value.
Cancel a subscription opened with #listen.
268 269 270 |
# File 'lib/mcp_client/server_base.rb', line 268 def cancel_subscription(subscription) raise NotImplementedError, 'Subclasses must implement cancel_subscription' end |
#capabilities ⇒ Hash?
Returns server capabilities.
127 128 129 |
# File 'lib/mcp_client/server_base.rb', line 127 def capabilities raise NotImplementedError, 'Subclasses must implement capabilities' end |
#capability?(*path) ⇒ Boolean
Whether the server declared the given (possibly nested) capability during initialization.
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/mcp_client/server_base.rb', line 136 def capability?(*path) node = begin capabilities rescue NotImplementedError nil end path.each do |key| return false unless node.is_a?(Hash) node = node[key.to_s] end !node.nil? && node != false end |
#cleanup ⇒ Object
Clean up the server connection
200 201 202 |
# File 'lib/mcp_client/server_base.rb', line 200 def cleanup raise NotImplementedError, 'Subclasses must implement cleanup' end |
#client_info=(info) ⇒ Object
Host-supplied Implementation info sent as clientInfo during initialize (MCP 2025-11-25 Implementation: name, version, plus optional title, description, websiteUrl, icons). Defaults to the gem's identity.
25 26 27 28 29 30 |
# File 'lib/mcp_client/server_base.rb', line 25 def client_info=(info) raise ArgumentError, 'client_info must include name' unless info['name'] || info[:name] raise ArgumentError, 'client_info must include version' unless info['version'] || info[:version] @client_info = info.transform_keys(&:to_s) end |
#connect ⇒ Boolean
Initialize a connection to the MCP server
40 41 42 |
# File 'lib/mcp_client/server_base.rb', line 40 def connect raise NotImplementedError, 'Subclasses must implement connect' end |
#discovery_refresh_needed? ⇒ Boolean
Whether the DiscoverResult behind the negotiated capabilities may still answer for the request about to go out.
It is a cacheable result like any other, so it is bound by both of the caching rules the lists and reads obey: its ttlMs, counted from receipt (a result with no hint, or a zero, negative or malformed one, is stale at once), and — for a privately scoped result, which is what a server that declares no scope gets — the authorization context and effective parameters of the request that produced it. "Private responses MUST NOT be shared across authorization contexts (e.g. a different access token requires a different cache)", and the capabilities a server declares are exactly the kind of answer that differs between two tokens.
185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/mcp_client/server_base.rb', line 185 def discovery_refresh_needed? return false unless respond_to?(:discovery_fresh?, true) return true unless discovery_fresh? return false unless respond_to?(:cache_fresh?, true) reusable = cache_fresh?(:discover) # The lookup holds the evaluation of the host's request_meta for the # request it expected to follow. Nothing is sent when the result stands, # so it is dropped rather than left on this thread for whichever request # goes out next (see ResultCaching#release_serving_request_meta). if reusable && respond_to?(:release_serving_request_meta, true) !reusable end |
#get_prompt(prompt_name, parameters) ⇒ Object
Get a prompt with the given parameters
68 69 70 |
# File 'lib/mcp_client/server_base.rb', line 68 def get_prompt(prompt_name, parameters) raise NotImplementedError, 'Subclasses must implement get_prompt' end |
#list_prompts ⇒ Array<MCPClient::Prompt>
List all prompts available from the MCP server
60 61 62 |
# File 'lib/mcp_client/server_base.rb', line 60 def list_prompts raise NotImplementedError, 'Subclasses must implement list_prompts' end |
#list_resource_templates(cursor: nil) ⇒ Hash
List all resource templates available from the MCP server
89 90 91 |
# File 'lib/mcp_client/server_base.rb', line 89 def list_resource_templates(cursor: nil) raise NotImplementedError, 'Subclasses must implement list_resource_templates' end |
#list_resources(cursor: nil) ⇒ Hash
List all resources available from the MCP server
75 76 77 |
# File 'lib/mcp_client/server_base.rb', line 75 def list_resources(cursor: nil) raise NotImplementedError, 'Subclasses must implement list_resources' end |
#list_tools ⇒ Array<MCPClient::Tool>
List all tools available from the MCP server
46 47 48 |
# File 'lib/mcp_client/server_base.rb', line 46 def list_tools raise NotImplementedError, 'Subclasses must implement list_tools' end |
#listen(notifications:, ack_timeout: nil) {|method, params| ... } ⇒ MCPClient::Subscription
Open a long-lived notification stream (MCP 2026-07-28 subscriptions/listen).
261 262 263 |
# File 'lib/mcp_client/server_base.rb', line 261 def listen(notifications:, ack_timeout: nil, &listener) raise NotImplementedError, 'Subclasses must implement listen' end |
#merge_related_task_meta(result, params) ⇒ Hash
Echo the related-task _meta of an incoming server request onto the outgoing result, so responses to task-related requests (elicitation or sampling during input_required) stay associated with their task.
118 119 120 121 122 123 124 |
# File 'lib/mcp_client/server_base.rb', line 118 def (result, params) = params.is_a?(Hash) ? params.dig('_meta', RELATED_TASK_META_KEY) : nil return result unless && result.is_a?(Hash) && !result.key?('error') = (result['_meta'] || {}).merge(RELATED_TASK_META_KEY => ) result.merge('_meta' => ) end |
#on_cache_invalidation {|method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback for the caches a notification invalidates, run before the notification is delivered to a subscription's listeners.
A host layered above the transport (MCPClient::Client) keeps caches of
its own, and they have to be gone by the time a listener reacting to a
list_changed notification calls the cached list method. on_notification
cannot serve for that: it is the last routing step, deliberately after
the delivery, because it is host code that may block on the very reader
the delivery came from. So the invalidation gets a hook of its own, ahead
of the delivery, and only the invalidation goes on it.
301 302 303 |
# File 'lib/mcp_client/server_base.rb', line 301 def on_cache_invalidation(&block) @cache_invalidation_callback = block end |
#on_notification {|method, params| ... } ⇒ void
This method returns an undefined value.
Register a callback to receive JSON-RPC notifications
284 285 286 |
# File 'lib/mcp_client/server_base.rb', line 284 def on_notification(&block) @notification_callback = block end |
#ping ⇒ Object
Ping the MCP server to check connectivity (zero-parameter heartbeat call)
277 278 279 |
# File 'lib/mcp_client/server_base.rb', line 277 def ping rpc_request('ping') end |
#read_resource(uri) ⇒ Array<MCPClient::ResourceContent>
Read a resource by its URI
82 83 84 |
# File 'lib/mcp_client/server_base.rb', line 82 def read_resource(uri) raise NotImplementedError, 'Subclasses must implement read_resource' end |
#require_capability!(*path, method:) ⇒ Object
Raise unless the server negotiated the given capability (MCP lifecycle: "Only use capabilities that were successfully negotiated").
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/mcp_client/server_base.rb', line 155 def require_capability!(*path, method:) # A DiscoverResult that may no longer be reused is re-fetched on the # next use of what it declared (MCP 2026-07-28 caching: a stale result # is re-fetched on access) BEFORE the capability is judged at all: the # server may have enabled a capability the old result lacked, or # withdrawn one it still lists. if respond_to?(:modern?, true) && modern? && discovery_refresh_needed? @logger&.debug("The server/discover result may not be reused; refreshing it before #{method}") rpc_request('server/discover') end return if capability?(*path) raise MCPClient::Errors::CapabilityError, "Server #{name || self.class.name} did not declare the #{path.join('.')} capability " \ "required for #{method}" end |
#resource_not_found_error(uri, error) ⇒ MCPClient::Errors::ResourceNotFound
Map a resources/read error response to ResourceNotFound. MCP 2026-07-28 (server/resources.mdx "Error Handling"): a missing resource is reported with -32602 (Invalid params); "for backwards compatibility, clients SHOULD also accept -32002 as a resource not found error".
312 313 314 |
# File 'lib/mcp_client/server_base.rb', line 312 def resource_not_found_error(uri, error) MCPClient::Errors::ResourceNotFound.new("Resource '#{uri}' not found: #{error.}") end |
#resource_not_found_response?(error) ⇒ Boolean
Whether a resources/read error response means the resource does not exist, given this session's protocol era.
320 321 322 323 |
# File 'lib/mcp_client/server_base.rb', line 320 def resource_not_found_response?(error) modern = respond_to?(:modern?) && modern? MCPClient::Errors::Codes.resource_not_found_code?(error.code, modern: modern) end |
#rpc_notify(method, params = {}) ⇒ void
This method returns an undefined value.
Send a JSON-RPC notification (no response expected)
236 237 238 |
# File 'lib/mcp_client/server_base.rb', line 236 def rpc_notify(method, params = {}) raise NotImplementedError, 'Subclasses must implement rpc_notify' end |
#rpc_request(method, params = {}) ⇒ Object
Send a JSON-RPC request and return the result
228 229 230 |
# File 'lib/mcp_client/server_base.rb', line 228 def rpc_request(method, params = {}) raise NotImplementedError, 'Subclasses must implement rpc_request' end |
#session_epoch ⇒ Integer
How many times this transport's session ended (cleanup, a restarted stdio process, a reconnect). Anything scoped to a session — task ids and their bookkeeping — is keyed by it, so state from a previous session never colours the next one.
209 210 211 |
# File 'lib/mcp_client/server_base.rb', line 209 def session_epoch @session_epoch || 0 end |
#subscribe_resource(uri) ⇒ Boolean
Subscribe to resource updates
96 97 98 |
# File 'lib/mcp_client/server_base.rb', line 96 def subscribe_resource(uri) raise NotImplementedError, 'Subclasses must implement subscribe_resource' end |
#unsubscribe_resource(uri) ⇒ Boolean
Unsubscribe from resource updates
103 104 105 |
# File 'lib/mcp_client/server_base.rb', line 103 def unsubscribe_resource(uri) raise NotImplementedError, 'Subclasses must implement unsubscribe_resource' end |