Module: Textus::Surface::MCP::Catalog

Defined in:
lib/textus/surface/mcp/catalog.rb

Overview

Derives the entire MCP tool surface from the per-verb contracts (ADR 0039). ‘build_tools` builds MCP::Tool instances for the SDK; `call` is the generic dispatch: map JSON args -> (positional, keyword) per the contract, invoke the verb through the role scope, then shape the return value. No per-tool code.

Constant Summary collapse

PROJECTOR =
Projector.new(view_key: :default, binder_method: :inputs_from_wire).freeze
WRITE_VERBS =
%i[
  put propose key_delete key_mv accept reject enqueue
].freeze
MAINTENANCE_VERBS =
%i[
  data_mv key_mv_prefix key_delete_prefix drain rule_lint
].freeze

Class Method Summary collapse

Class Method Details

.build_tools(mcp_server) ⇒ Object

Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/textus/surface/mcp/catalog.rb', line 29

def build_tools(mcp_server)
  Textus::Action::VERBS
    .select { |_, klass| mcp_surfaced?(klass) }
    .map do |name, action|
      schema = action.contract.input_schema
      schema = schema.reject { |k, v| k == :required && Array(v).empty? }
      ::MCP::Tool.define(
        name: name.to_s,
        description: action.contract.summary,
        input_schema: schema,
      ) do |server_context:, **args|
        mcp_server.dispatch(name, args, server_context)
      end
    end
end

.call(name, session:, store:, args:) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/textus/surface/mcp/catalog.rb', line 78

def call(name, session:, store:, args:)
  klass = Textus::Action::VERBS[name.to_sym]
  raise ToolError.new("unknown tool: #{name}") unless klass && mcp_surfaced?(klass)

  PROJECTOR.dispatch(name, inputs: args, store:, role: session.role, session:)
rescue Textus::Gate::MissingArgs => e
  spec = klass.contract
  raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
rescue Textus::ContractDrift, CursorExpired
  raise
rescue Textus::Error => e
  raise ToolError.new("#{name}: #{e.message}")
end

.mcp_surfaced?(klass) ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/textus/surface/mcp/catalog.rb', line 74

def mcp_surfaced?(klass)
  klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp?
end

.namesObject



45
46
47
48
49
# File 'lib/textus/surface/mcp/catalog.rb', line 45

def names
  PROJECTOR.names(
    Textus::Action::VERBS.select { |_, klass| mcp_surfaced?(klass) },
  )
end

.read_verbsObject

MCP-surfaced read verbs, by Dispatcher class namespace — the agent’s real read/discovery surface. ‘boot.agent_quickstart.read_verbs` derives from this so it can never advertise a verb the agent cannot call, nor omit one it can (ADR 0056). Excludes write/maintenance verbs by verb identity (routing may be legacy UseCases or Dispatch::Actions).



56
57
58
59
60
61
# File 'lib/textus/surface/mcp/catalog.rb', line 56

def read_verbs
  Textus::Action::VERBS
    .reject { |verb, _klass| WRITE_VERBS.include?(verb) || MAINTENANCE_VERBS.include?(verb) }
    .select { |_verb, klass| mcp_surfaced?(klass) }
    .keys.map(&:to_s)
end

.specsObject

Contracts of every MCP-surfaced verb, in Dispatcher order.



22
23
24
25
26
# File 'lib/textus/surface/mcp/catalog.rb', line 22

def specs
  Textus::Action::VERBS.values
                       .select { |k| mcp_surfaced?(k) }
                       .map(&:contract)
end

.write_verbsObject

MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of read_verbs for the write side. ‘boot.agent_quickstart.write_verbs` derives from this so it advertises bare verb names the agent can call (no `–as`/ `–stdin` CLI framing), finishing the de-CLI-ing of the agent surface (ADR 0056, ADR 0057).



68
69
70
71
72
# File 'lib/textus/surface/mcp/catalog.rb', line 68

def write_verbs
  Textus::Action::VERBS
    .select { |verb, klass| WRITE_VERBS.include?(verb) && mcp_surfaced?(klass) }
    .keys.map(&:to_s)
end