Module: Textus::Surfaces::MCP::Catalog

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

Overview

Derives the entire MCP tool surface from the per-verb contracts (ADR 0039). ‘tool_schemas` feeds tools/list; `call` is the generic tools/call dispatch: map JSON args -> (positional, keyword) per the contract, invoke the verb through the role scope, then shape the return value with the contract’s default view. No per-tool code.

Constant Summary collapse

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

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

rubocop:disable Metrics/AbcSize



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/textus/surfaces/mcp/catalog.rb', line 64

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

  spec = klass.contract
  inputs = Textus::Contract::Binder.inputs_from_wire(spec, args)

  invoke = lambda do |effective_inputs|
    pos, kwargs = Textus::Contract::Binder.bind(spec, effective_inputs, session: session)
    spec.args.select(&:positional).zip(pos).each { |a, v| kwargs[a.name] = v unless kwargs.key?(a.name) }
    cmd_class = Textus::Gate::VERB_COMMAND.fetch(spec.verb) do
      raise Textus::MCP::ToolError.new("unknown verb: #{spec.verb}")
    end
    merged = kwargs.merge(role: session.role)
    filled = cmd_class.members.to_h { |m| [m, merged.key?(m) ? merged[m] : nil] }
    cmd = cmd_class.new(**filled)
    store.gate.dispatch(cmd)
  end

  result = if spec.around
             Textus::Contract::Around.with(spec.around, scope: store.as(session.role), inputs: inputs, session: session, &invoke)
           else
             invoke.call(inputs)
           end
  Textus::Contract::View.render(spec, :default, result, inputs)
rescue Textus::Contract::MissingArgs => e
  raise ToolError.new("#{spec.verb}: missing #{e.missing.map { |a| a.wire.to_s }.join(", ")}")
rescue ContractDrift, CursorExpired
  raise
rescue Textus::Error => e
  raise ToolError.new("#{name}: #{e.message}")
end

.mcp_surfaced?(klass) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/textus/surfaces/mcp/catalog.rb', line 60

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

.namesObject



33
34
35
# File 'lib/textus/surfaces/mcp/catalog.rb', line 33

def names
  specs.map { |s| s.verb.to_s }
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).



42
43
44
45
46
47
# File 'lib/textus/surfaces/mcp/catalog.rb', line 42

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.



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

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

.tool_schemasObject



27
28
29
30
31
# File 'lib/textus/surfaces/mcp/catalog.rb', line 27

def tool_schemas
  specs.map do |s|
    { name: s.verb.to_s, description: s.summary, inputSchema: s.input_schema }
  end.freeze
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).



54
55
56
57
58
# File 'lib/textus/surfaces/mcp/catalog.rb', line 54

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