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). ‘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
- 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
-
.build_tools(mcp_server) ⇒ Object
Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
-
.call(name, session:, store:, args:) ⇒ Object
rubocop:disable Metrics/AbcSize.
- .mcp_surfaced?(klass) ⇒ Boolean
- .names ⇒ Object
-
.read_verbs ⇒ Object
MCP-surfaced read verbs, by Dispatcher class namespace — the agent’s real read/discovery surface.
-
.specs ⇒ Object
Contracts of every MCP-surfaced verb, in Dispatcher order.
-
.write_verbs ⇒ Object
MCP-surfaced write verbs, by Dispatcher class namespace — the mirror of read_verbs for the write side.
Class Method Details
.build_tools(mcp_server) ⇒ Object
Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 27 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
rubocop:disable Metrics/AbcSize
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 76 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 Textus::ContractDrift, CursorExpired raise rescue Textus::Error => e raise ToolError.new("#{name}: #{e.}") end |
.mcp_surfaced?(klass) ⇒ Boolean
72 73 74 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 72 def mcp_surfaced?(klass) klass.respond_to?(:contract?) && klass.contract? && klass.contract.mcp? end |
.names ⇒ Object
43 44 45 46 47 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 43 def names Textus::Action::VERBS .select { |_, klass| mcp_surfaced?(klass) } .keys.map(&:to_s) end |
.read_verbs ⇒ Object
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).
54 55 56 57 58 59 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 54 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 |
.specs ⇒ Object
Contracts of every MCP-surfaced verb, in Dispatcher order.
20 21 22 23 24 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 20 def specs Textus::Action::VERBS.values .select { |k| mcp_surfaced?(k) } .map(&:contract) end |
.write_verbs ⇒ Object
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).
66 67 68 69 70 |
# File 'lib/textus/surfaces/mcp/catalog.rb', line 66 def write_verbs Textus::Action::VERBS .select { |verb, klass| WRITE_VERBS.include?(verb) && mcp_surfaced?(klass) } .keys.map(&:to_s) end |