Module: Jazari::Mcp::Actions

Defined in:
lib/jazari/mcp/actions.rb

Overview

Declarative descriptors for every action, so a host can absorb jazari into ITS OWN tool instead of adopting this gem's handler.

Without these, a host has two bad choices: take Mcp::Handler's reply shape wholesale, or hand-write the action list and let it drift from the gem it describes. Neither is acceptable for a host that already has an MCP surface with its own envelope, naming, and permission model.

These descriptors are the single source of truth: Handler validates and scopes against them too, so the schema a host publishes and the behaviour the gem implements cannot diverge.

Defined Under Namespace

Classes: Action

Constant Summary collapse

REVISION =
{ type: "string",
description: "The revision from the read immediately before this call." }.freeze
ITEM_ID =
{ type: "string", description: "Opaque checklist item id." }.freeze
RUN_ID =
{ type: "integer", description: "The run returned by start." }.freeze
ACTOR =
{ type: "string", description: "Opaque identity of who is acting." }.freeze
ALL =
[
  Action.new(name: "get", scope: :read, effect: :read, confirm: false,
             summary: "Resolve the operating procedure for a target, with progress and last run.",
             params: {}),
  Action.new(name: "last_run", scope: :read, effect: :read, confirm: false,
             summary: "The most recent run for a target — answers whether the ritual actually happened.",
             params: {}),
  Action.new(name: "set", scope: :write, effect: :overwrite, confirm: false,
             summary: "Replace this subject's procedure. Materialises an override on first use.",
             params: { expected_revision: REVISION,
                       topic: { type: "string", description: "Short title." },
                       description: { type: "string", description: "Markdown body." },
                       checklist: { type: "array", description: "Full checklist; replaces the existing one." } }),
  Action.new(name: "add_item", scope: :write, effect: :additive, confirm: false,
             summary: "Append one checklist step.",
             params: { expected_revision: REVISION,
                       text: { type: "string", description: "The step." },
                       required: { type: "boolean", description: "Whether the step is required. Default true." } }),
  Action.new(name: "remove_item", scope: :write, effect: :destructive, confirm: false,
             summary: "Delete one checklist step.",
             params: { expected_revision: REVISION, item_id: ITEM_ID }),
  Action.new(name: "check_item", scope: :write, effect: :additive, confirm: false,
             summary: "Mark a step done or not done.",
             params: { expected_revision: REVISION, item_id: ITEM_ID,
                       done: { type: "boolean", description: "Default true." } }),
  Action.new(name: "reset", scope: :write, effect: :destructive, confirm: true,
             summary: "Discard this subject's override and reveal the current canon.",
             params: { expected_revision: REVISION,
                       confirm: { type: "boolean", description: "Must be true — this discards operator content." } }),
  Action.new(name: "start", scope: :write, effect: :additive, confirm: false,
             summary: "Open a run. Under a once-per-day recipe this returns the existing run instead of erroring.",
             params: { actor_ref: ACTOR }),
  Action.new(name: "tick", scope: :write, effect: :additive, confirm: false,
             summary: "Record a step done within a run. Does not touch the subject's own checklist.",
             params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
                       done: { type: "boolean", description: "Default true." },
                       actor_ref: ACTOR,
                       note: { type: "string", description: "Optional free text." } }),
  Action.new(name: "evidence", scope: :write, effect: :additive, confirm: false,
             summary: "Attach evidence to a run: output, url, sha, count, or note.",
             params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
                       kind: { type: "string", description: "One of: output, url, sha, count, note." },
                       value: { type: "string", description: "The evidence itself." },
                       actor_ref: ACTOR }),
  Action.new(name: "finish", scope: :write, effect: :additive, confirm: false,
             summary: "Close a run with an outcome: completed, abandoned, or failed.",
             params: { run_id: RUN_ID, expected_revision: REVISION,
                       outcome: { type: "string", description: "completed | abandoned | failed" } })
].freeze
NAMES =
ALL.map(&:name).freeze

Class Method Summary collapse

Class Method Details

.all(scope: :write) ⇒ Object

scope: :read advertises only the read-only subset. A read-scoped connection should not SEE mutations in its tool list — offering them and then refusing is a worse experience than not offering them.



92
93
94
# File 'lib/jazari/mcp/actions.rb', line 92

def all(scope: :write)
  scope.to_s == "read" ? ALL.select(&:read?) : ALL
end

.fetch(name) ⇒ Object



98
99
100
101
# File 'lib/jazari/mcp/actions.rb', line 98

def fetch(name)
  ALL.find { |action| action.name == name.to_s } or
    raise ArgumentError, "unknown runbook action #{name.inspect}"
end

.names(scope: :write) ⇒ Object



96
# File 'lib/jazari/mcp/actions.rb', line 96

def names(scope: :write) = all(scope: scope).map(&:name)

.schema_fragment(scope: :write) ⇒ Object

A fragment a host merges into its OWN tool's input schema. It deliberately returns only the action enum and the parameter properties — the host owns the tool name, the description, its own target params, and its reply envelope. Nothing here presumes this gem's handler is in the loop.

frag = Jazari::Mcp::Actions.schema_fragment
MY_TOOL[:input_schema][:properties].merge!(frag[:properties])
MY_TOOL[:input_schema][:properties][:action][:enum] += frag[:enum]


111
112
113
114
115
116
117
# File 'lib/jazari/mcp/actions.rb', line 111

def schema_fragment(scope: :write)
  actions = all(scope: scope)
  properties = actions.each_with_object({}) do |action, acc|
    action.params.each { |key, spec| acc[key] ||= spec }
  end
  { enum: actions.map(&:name), properties: properties }
end

.summaries(scope: :write) ⇒ Object

Human-readable action list for a tool description or a paired skill.



120
121
122
# File 'lib/jazari/mcp/actions.rb', line 120

def summaries(scope: :write)
  all(scope: scope).map { |a| "#{a.name}#{a.summary}" }
end