Module: Cuboid::MCP::CoreTools

Defined in:
lib/cuboid/mcp/core_tools.rb

Overview

Framework-level MCP tools — instance management. Mounted by ‘Cuboid::MCP::Server::Dispatcher` at the top-level `/mcp` endpoint so an MCP-only client has a way to spawn / list / kill engine instances without going through the REST surface.

Per-instance per-service tools (the application-gem-supplied ones registered via ‘mcp_service_for`) live at `/instances/:instance/<service>` and pick up where these leave off: the typical client lifecycle is

spawn_instance → returns instance_id
POST /instances/<instance_id>/<service> { tools/call ... }
...
kill_instance(id: instance_id)

Defined Under Namespace

Classes: KillInstance, ListInstances, SpawnInstance

Constant Summary collapse

TOOLS =
[ ListInstances, SpawnInstance, KillInstance ].freeze

Class Method Summary collapse

Class Method Details

.extract_session_id(server_context) ⇒ Object

‘MCP::ServerContext` doesn’t expose its ‘notification_target` publicly — the only readers are inside the gem. Reach through to the wrapped session for its session_id; if anything in this chain isn’t there (stateless transport, bare invocation) we bail and the live injection is skipped.



221
222
223
224
225
226
# File 'lib/cuboid/mcp/core_tools.rb', line 221

def self.extract_session_id( server_context )
    return nil if server_context.nil?
    target = server_context.instance_variable_get( :@notification_target )
    return nil if target.nil? || !target.respond_to?( :session_id )
    target.session_id
end

.inject_live_plugin(options, instance_id:, server_context:) ⇒ Object

Mutate (a copy of) the user’s ‘options` so the engine subprocess loads the `live` plugin pointed at this MCP server’s ‘/mcp/live/<token>` route. Honors anything the user explicitly set under `plugins.live` (metadata, serializer); only the `url` is auto-injected. Skips the injection silently if the call didn’t arrive over a session that can receive notifications —‘live: true` over a stateless / non-MCP transport has nowhere to send events, so we let the spawn proceed without it.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/cuboid/mcp/core_tools.rb', line 188

def self.inject_live_plugin( options, instance_id:, server_context: )
    session_id = extract_session_id( server_context )
    return options if !session_id
    return options if !::Cuboid::MCP::Live.configured?

    token = ::Cuboid::MCP::Live.register(
        session_id:  session_id,
        instance_id: instance_id
    )

    options = (options || {}).dup
    # Normalise plugins to Hash{String => Hash} so the merge below
    # is well-defined regardless of whether the caller supplied
    # Hash, Array, or nothing.
    plugins = case options['plugins']
              when Hash  then options['plugins'].dup
              when Array then options['plugins'].each_with_object({}) { |n, h| h[n.to_s] = {} }
              else            {}
              end

    live_opts        = (plugins['live'] || {}).dup
    live_opts['url'] = ::Cuboid::MCP::Live.url_for( token )
    plugins['live']  = live_opts

    options['plugins'] = plugins
    options
end

.instancesObject

Direct access to the shared in-memory map populated by REST POST /instances + the scheduler-sync flow + spawn_instance below. Module-level so tools don’t have to mix in the InstanceHelpers context (which carries Sinatra-helper assumptions like ‘session`).



29
30
31
# File 'lib/cuboid/mcp/core_tools.rb', line 29

def self.instances
    ::Cuboid::Server::InstanceHelpers.instances
end

.instrumented_callObject

Wraps a tool body. Returns an MCP::Tool::Response that always carries a JSON-encoded ‘text` content for clients that don’t yet consume ‘structuredContent`, and — when the result is structured (Hash/Array) — also a `structuredContent` block matching the tool’s ‘output_schema`. A raised exception is captured and returned as an MCP error response so the MCP server itself stays up for the next call.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/cuboid/mcp/core_tools.rb', line 40

def self.instrumented_call
    result = yield

    if result.is_a?( String )
        ::MCP::Tool::Response.new(
            [{ type: 'text', text: result }]
        )
    else
        ::MCP::Tool::Response.new(
            [{ type: 'text', text: JSON.pretty_generate( result ) }],
            structured_content: result
        )
    end
rescue => e
    ::MCP::Tool::Response.new(
        [{ type: 'text', text: "error: #{e.class}: #{e.message}" }],
        error: true
    )
end

.toolsObject



311
312
313
# File 'lib/cuboid/mcp/core_tools.rb', line 311

def self.tools
    TOOLS
end