Class: MCPClient::ServerBase

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp_client/server_base.rb

Overview

Base class for MCP servers - serves as the interface for different server implementations

Constant Summary collapse

'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

Instance Method Summary collapse

Constructor Details

#initialize(name: nil) ⇒ ServerBase

Initialize the server with a name

Parameters:

  • name (String, nil) (defaults to: nil) —

    server 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

Returns:

  • (String, nil)


18
19
20
# File 'lib/mcp_client/server_base.rb', line 18

def instructions
  @instructions
end

#logger ⇒ Logger (readonly)

Returns the transport logger.

Returns:

  • (Logger) —

    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

Parameters:

  • tool_name (String) —

    the name of the tool to call

  • parameters (Hash) —

    the parameters to pass to the tool

Returns:

  • (Object) —

    the result of the tool invocation

Raises:

  • (NotImplementedError)


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)

Parameters:

  • tool_name (String) —

    the name of the tool to call

  • parameters (Hash) —

    the parameters to pass to the tool

Returns:

  • (Enumerator) —

    stream of results



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.

Parameters:

Raises:

  • (NotImplementedError)


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.

Returns:

  • (Hash, nil) —

    server capabilities

Raises:

  • (NotImplementedError)


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.

Parameters:

  • path (Array<String, Symbol>) —

    capability key path, e.g. 'logging' or 'resources', 'subscribe'

Returns:

  • (Boolean)


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

Raises:

  • (NotImplementedError)


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.

Parameters:

  • info (Hash) —

    implementation info; must include name and version

Raises:

  • (ArgumentError) —

    when name or version is missing



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

Returns:

  • (Boolean) —

    true if connection successful

Raises:

  • (NotImplementedError)


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.

Returns:

  • (Boolean)


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).
  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

Parameters:

  • prompt_name (String) —

    the name of the prompt to get

  • parameters (Hash) —

    the parameters to pass to the prompt

Returns:

  • (Object) —

    the result of the prompt interpolation

Raises:

  • (NotImplementedError)


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

Returns:

Raises:

  • (NotImplementedError)


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

Parameters:

  • cursor (String, nil) (defaults to: nil) —

    optional cursor for pagination

Returns:

  • (Hash) —

    result containing resourceTemplates array and optional nextCursor

Raises:

  • (NotImplementedError)


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

Parameters:

  • cursor (String, nil) (defaults to: nil) —

    optional cursor for pagination

Returns:

  • (Hash) —

    result containing resources array and optional nextCursor

Raises:

  • (NotImplementedError)


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

Returns:

Raises:

  • (NotImplementedError)


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).

Parameters:

  • notifications (Hash) —

    the SubscriptionFilter (tools_list_changed, prompts_list_changed, resources_list_changed, resource_subscriptions, snake_case or camelCase; an extension's own field, such as the tasks extension's task_ids, once it has registered it — see MCPClient::Subscription.register_filter_field)

  • ack_timeout (Numeric, false, nil) (defaults to: nil) —

    seconds to wait for the server's acknowledgment before giving the listen up; nil takes the transport's own read timeout, false waits for ever

Yields:

  • (method, params) —

    notifications delivered on the subscription

Returns:

Raises:

  • (NotImplementedError)


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

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.

Parameters:

  • result (Hash) —

    the outgoing JSON-RPC result payload

  • params (Hash, nil) —

    the incoming request params

Returns:

  • (Hash) —

    result with related-task _meta merged when applicable



118
119
120
121
122
123
124
# File 'lib/mcp_client/server_base.rb', line 118

def merge_related_task_meta(result, params)
  related = params.is_a?(Hash) ? params.dig('_meta', RELATED_TASK_META_KEY) : nil
  return result unless related && result.is_a?(Hash) && !result.key?('error')

  meta = (result['_meta'] || {}).merge(RELATED_TASK_META_KEY => related)
  result.merge('_meta' => 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.

Yields:

  • (method, params) —

    invoked before the notification is delivered

See Also:



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

Yields:

  • (method, params) —

    invoked when a notification is received



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)

Returns:

  • (Object) —

    result from the ping request



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

Parameters:

  • uri (String) —

    the URI of the resource to read

Returns:

Raises:

  • (NotImplementedError)


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").

Parameters:

  • path (Array<String, Symbol>) —

    capability key path

  • method (String) —

    the JSON-RPC method the caller wants to send

Raises:



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".

Parameters:

Returns:



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.message}")
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.

Parameters:

Returns:

  • (Boolean)


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)

Parameters:

  • method (String) —

    JSON-RPC method name

  • params (Hash) (defaults to: {}) —

    parameters for the notification

Raises:

  • (NotImplementedError)


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

Parameters:

  • method (String) —

    JSON-RPC method name

  • params (Hash) (defaults to: {}) —

    parameters for the request

Returns:

  • (Object) —

    result field from the JSON-RPC response

Raises:



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.

Returns:

  • (Integer)


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

Parameters:

  • uri (String) —

    the URI of the resource to subscribe to

Returns:

  • (Boolean) —

    true if subscription successful

Raises:

  • (NotImplementedError)


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

Parameters:

  • uri (String) —

    the URI of the resource to unsubscribe from

Returns:

  • (Boolean) —

    true if unsubscription successful

Raises:

  • (NotImplementedError)


103
104
105
# File 'lib/mcp_client/server_base.rb', line 103

def unsubscribe_resource(uri)
  raise NotImplementedError, 'Subclasses must implement unsubscribe_resource'
end