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
- ID_PATTERN =
JSON Schema patterns are ECMAScript, which has no
\A/\z. Publishing Ruby's source verbatim hands validators an anchor they may reject or, worse, read as a literal — so translate the anchors and keep everything else derived from ID_FORMAT, which stays the single source of truth. Checklist::ID_FORMAT.source.sub('\\A', "^").sub('\\z', "$").freeze
- ITEM_ID =
{ type: "string", description: "Opaque checklist item id." }.freeze
- CHECKLIST_ITEM =
An
arraywith noitemsis not a contract, it is a guess. A client generating from that descriptor is free to inferstring[], send["step one"], and be rejected by the domain with "checklist item must be a hash" — which is what happened. The schema must publish the shape the validator actually enforces, or the descriptor is documentation that disagrees with the code. { type: "object", required: [ "text" ], properties: { id: { type: "string", pattern: ID_PATTERN, maxLength: 64, description: "Opaque id. Omit to have one generated; send it back to keep a step " \ "stable across edits. An id that does not match the pattern is REPLACED " \ "with a generated one rather than rejected." }, # minLength/maxLength, not just "string": empty text and 501 characters # are both rejected by the domain, and a schema that admits them makes # the client discover it at call time. text: { type: "string", minLength: 1, maxLength: Checklist::MAX_TEXT, description: "The step. Required, non-empty." }, done: { type: "boolean", description: "Default false." }, required: { type: "boolean", description: "Whether the step gates completion. Default true." } }, additionalProperties: false }.freeze
- CHECKLIST =
The one domain rule JSON Schema cannot express is the aggregate byte bound, so it is DISCLOSED rather than left for the client to discover by being rejected. An undocumented limit is the same failure as an untyped array: the schema knows something the caller does not.
{ type: "array", items: CHECKLIST_ITEM, description: "Full checklist; replaces the existing one. There is no row limit, " \ "but the serialized checklist must be at most " \ "#{Checklist::MAX_PAYLOAD} bytes." }.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: CHECKLIST }), 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
-
.all(scope: :write) ⇒ Object
scope: :read advertises only the read-only subset.
- .fetch(name) ⇒ Object
- .names(scope: :write) ⇒ Object
-
.schema_fragment(scope: :write) ⇒ Object
A fragment a host merges into its OWN tool's input schema.
-
.summaries(scope: :write) ⇒ Object
Human-readable action list for a tool description or a paired skill.
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.
138 139 140 |
# File 'lib/jazari/mcp/actions.rb', line 138 def all(scope: :write) scope.to_s == "read" ? ALL.select(&:read?) : ALL end |
.fetch(name) ⇒ Object
144 145 146 147 |
# File 'lib/jazari/mcp/actions.rb', line 144 def fetch(name) ALL.find { |action| action.name == name.to_s } or raise ArgumentError, "unknown runbook action #{name.inspect}" end |
.names(scope: :write) ⇒ Object
142 |
# File 'lib/jazari/mcp/actions.rb', line 142 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]
157 158 159 160 161 162 163 |
# File 'lib/jazari/mcp/actions.rb', line 157 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.
166 167 168 |
# File 'lib/jazari/mcp/actions.rb', line 166 def summaries(scope: :write) all(scope: scope).map { |a| "#{a.name} — #{a.summary}" } end |