MCP tools

Reference for the two fixed MCP tools that praxec exposes to your model: praxec.query (reads) and praxec.command (writes).

No matter how many capabilities you wire in, the model always sees exactly two tools: praxec.query for all reads and praxec.command for all state-changing writes. The split follows CQRS — if it has side effects, it’s a command; if it doesn’t, it’s a query.

The tool name carries nearly no decision weight for the model. What does the work is the links[] array every response carries: pre-filled { method, args } objects the model copies verbatim to make the next call. The schema declares what’s valid; the response declares what’s valid right now.


praxec.query

Side-effect-free reads. Covers: catalog browsing (home), full-text search, fragment describe, workflow get, workflow explain, and lexicon reads.

Schema — all fields optional; the combination of fields present selects the operation:

{
  "query":      "string",
  "kind":       "string",
  "subject":    "string",
  "workflowId": "string",
  "transition": "string",
  "limit":      "integer"
}

Dispatch table

Args present Operation
(none) home — entry point; links to search and list operations
query search — full-text scored results
subject only describe — live-config fragment or capability
subject + workflowId describe — against the instance’s pinned snapshot (§8.2)
workflowId alone get — current workflow state
workflowId + transition explain — inspect a transition definition

kind and limit are optional modifiers on search; they don’t change which dispatch row matches. kind filters the result set. The always-available values are workflow, capability, and connection. lexicon is also accepted and routes to a lexicon search. skill and script are authoring-time only and flag-gated — they return INVALID_PARAMS unless the server was started with with_skills_search(true) / with_scripts_search(true). limit caps the result count.

Audit: every describe call emits a guidance.describe_requested audit event — including browse-time describe (subject alone), where the event’s workflowId is null. When the call includes both subject AND workflowId, the event carries that workflowId — i.e., “guidance fetched for use in a running workflow.”


home

Returns the full capability catalog. Entry point for a cold-start model.

Example request:

{
  "name": "praxec.query",
  "arguments": {}
}

Example response:

{
  "resource": { "type": "gateway", "id": "home" },
  "result": {
    "status": "ready",
    "message": "Available workflows and proxy capabilities can be discovered here."
  },
  "links": [
    {
      "rel": "search",
      "title": "Search workflows and capabilities",
      "method": "praxec.query",
      "args": { "query": "" },
      "inputSchema": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": { "type": "string" },
          "kind": { "type": "string", "enum": ["workflow", "capability", "connection"] },
          "limit": { "type": "integer", "default": 10 }
        },
        "additionalProperties": false
      }
    },
    {
      "rel": "list_workflows",
      "title": "List configured workflows",
      "method": "praxec.query",
      "args": { "query": "", "kind": "workflow" }
    },
    {
      "rel": "list_capabilities",
      "title": "List proxy capabilities",
      "method": "praxec.query",
      "args": { "query": "", "kind": "capability" }
    }
  ]
}

home is a thin entry point, not a catalog dump. It hands the model three links — search, list_workflows, list_capabilities — and the model follows one to enumerate what’s actually available. The model chains these instead of guessing call shapes.


Full-text search across all indexed capabilities, workflows, skills, scripts, and lexicon entries.

Example request:

{
  "name": "praxec.query",
  "arguments": {
    "query": "deploy",
    "kind": "workflow",
    "limit": 5
  }
}

Example response:

{
  "query": "deploy",
  "kind": "workflow",
  "items": [
    {
      "score": 14.0,
      "item": {
        "id": "deploy_pipeline",
        "kind": "workflow",
        "title": "Deploy Pipeline",
        "description": "Lint, test, build, and deploy a service.",
        "tags": ["deploy", "pipeline"],
        "links": [
          {
            "rel": "start",
            "method": "praxec.command",
            "args": { "definitionId": "deploy_pipeline", "input": {} }
          }
        ]
      }
    }
  ],
  "links": [
    { "rel": "home", "method": "praxec.query", "args": {} }
  ]
}

Scoring weights:

Field Weight What it matches
title 6.0 Capability or workflow title
id 5.0 The identifier
tags 3.0 Tags array
aliases 3.0 Search synonyms declared in config
description 2.0 Description text
text 1.0 State names, transition names, guidance text

Lexicon search — pass kind: "lexicon" to restrict to the lexicon:

{ "query": "evidence", "kind": "lexicon" }

Returns { "hits": [...] }, where each hit is the full stored lexicon entry with term added — e.g. { "term", "definition_short", "bounded_context", "refs"?, "governance"?, "state"? }. The exact keys depend on what the entry was authored with (an aliases array, for instance, appears only when the entry declares one); a hit is not a fixed subset.


describe

Returns the full details for a single capability, workflow definition, skill, script, or lexicon entry.

Example request:

{
  "name": "praxec.query",
  "arguments": {
    "subject": "github.list_issues"
  }
}

Example response:

For a workflow, capability, or connection, describe wraps the matched DiscoveryItem under item and attaches navigation links:

{
  "id": "github.list_issues",
  "item": {
    "id": "github.list_issues",
    "kind": "capability",
    "title": "List GitHub issues",
    "description": "List issues from a GitHub repository.",
    "tags": ["github", "issues", "read"],
    "links": [
      {
        "rel": "start",
        "method": "praxec.command",
        "args": { "definitionId": "proxy_default", "input": { "capability": "github.list_issues" } },
        "input_schema": {
          "type": "object",
          "required": ["repo"],
          "properties": {
            "repo": { "type": "string" }
          }
        }
      }
    ]
  },
  "links": [
    { "rel": "home",   "method": "praxec.query", "args": {} },
    { "rel": "search", "method": "praxec.query", "args": { "query": "" } }
  ]
}

Guidance (skill) and script subjects return a flatter wire shape instead — { "kind", "subject", "verb", "body", "links", "lexicon"? } — since they carry a guidance body rather than a HATEOAS-driven DiscoveryItem.

Lexicon lookup — use the lexicon: subject prefix to look up a term:

{ "subject": "lexicon:evidence-pack" }

Returns { "term", "entry" } where entry may be null if the term has no definition yet. A lookup against any registered alias returns the same entry as the canonical term.


get

Check the current state of a running workflow.

Example request:

{
  "name": "praxec.query",
  "arguments": {
    "workflowId": "wf_a1b2c3d4e5f6"
  }
}

Example response:

{
  "workflow": {
    "id": "wf_a1b2c3d4e5f6",
    "definitionId": "deploy_pipeline",
    "definitionVersion": 1,
    "state": "ready_to_deploy",
    "version": 4
  },
  "result": { "status": "waiting_for_action" },
  "context": {
    "lintPassed": true,
    "testsPassed": true,
    "coverage": 87.5,
    "artifactId": "art_xyz789"
  },
  "links": [
    {
      "rel": "deploy",
      "title": "Deploy to environment",
      "description": null,
      "method": "praxec.command",
      "actor": "agent",
      "args": {
        "workflowId": "wf_a1b2c3d4e5f6",
        "expectedVersion": 4,
        "transition": "deploy"
      },
      "inputSchema": { "type": "object" }
    },
    {
      "rel": "abort",
      "title": "Abort deployment",
      "description": null,
      "method": "praxec.command",
      "actor": "agent",
      "args": {
        "workflowId": "wf_a1b2c3d4e5f6",
        "expectedVersion": 4,
        "transition": "abort"
      },
      "inputSchema": { "type": "object" }
    }
  ],
  "evidence": []
}

The instance lives under workflow — workflow.id, workflow.state, workflow.version. The context accumulates data from previous transitions. Each link’s rel is the transition name (deploy, abort), and args.transition repeats it. The links show every legal next move; a link only carries args.arguments when the transition declares prefill.


explain

Inspect a workflow definition or a specific transition without starting anything.

Example request (specific transition):

{
  "name": "praxec.query",
  "arguments": {
    "workflowId": "wf_a1b2c3d4e5f6",
    "transition": "deploy"
  }
}

Example response:

{
  "workflowId": "wf_a1b2c3d4e5f6",
  "currentState": "ready_to_deploy",
  "transition": "deploy",
  "allowedFromCurrentState": true,
  "actor": "agent",
  "deterministic": false,
  "legalTransitionsNow": ["deploy", "abort"]
}

explain answers “can I fire this transition from where the instance is right now, and what else is legal?” — allowedFromCurrentState is the verdict, legalTransitionsNow lists the transition names available from currentState, and deterministic is true when the transition’s actor is deterministic (the runtime would chain through it automatically). It does not expose the transition’s guards, target state, input schema, or executor.


Subject namespace

The subject field admits a colon-prefixed namespace. Only the lexicon: prefix is interpreted specially by query-time describe; everything else is resolved by an exact match on the discovery index id:

Prefix Resolves to
(none) guidance fragment, workflow definition, capability, script, or connection — matched by exact id against the discovery index
lexicon:<term> a single lexicon entry (the only prefix the query path special-cases)
workflow:<id> reserved; not resolved as a prefix — the runtime matches workflow:<id> literally as an id and finds nothing unless an item is named that
script:<subject> reserved; not resolved as a prefix (same literal-id behaviour)
skill:<subject> reserved; not resolved as a prefix (same literal-id behaviour)

Pass unprefixed subjects for workflows, capabilities, connections, scripts, and guidance — the discovery index resolves them by id. The lexicon: prefix is the only one that switches resolution path.


praxec.command

State-changing writes. Covers: workflow start, workflow transition submit, and lexicon define (when enabled).

Schema — all fields optional; the combination of fields present selects the operation:

{
  "definitionId":    "string",
  "input":           "object",
  "workflowId":      "string",
  "expectedVersion": "integer",
  "transition":      "string",
  "arguments":       "object",
  "subject":         "string",
  "definition":      "object",
  "summary":         "string",
  "traceId":         "string",
  "runId":           "string",
  "intent":          "string",
  "unknown_subject": "string"
}

Dispatch table

Args present Operation
definitionId, no workflowId, no subject start — create a new workflow instance
workflowId + transition + expectedVersion submit — fire a transition
subject (namespaced) + definition define — write a lexicon entry

input and runId are modifiers on start. arguments and summary are modifiers on submit. traceId and runId thread through to audit on any operation. intent plus unknown_subject drive the out-of-band cancel_pending_subject resolution (note unknown_subject keeps its snake_case wire key to match the cancel link emitted by SUBJECT_NEEDS_DEFINITION).


start

Create a new workflow instance at its initial state.

Example request:

{
  "name": "praxec.command",
  "arguments": {
    "definitionId": "deploy_pipeline",
    "input": {
      "service": "payments",
      "environment": "staging"
    }
  }
}

Example response:

{
  "workflow": {
    "id": "wf_a1b2c3d4e5f6",
    "definitionId": "deploy_pipeline",
    "definitionVersion": 1,
    "state": "lint",
    "version": 1
  },
  "result": { "status": "started" },
  "context": {},
  "links": [
    {
      "rel": "run_lint",
      "title": "Run linter",
      "description": null,
      "method": "praxec.command",
      "actor": "agent",
      "args": {
        "workflowId": "wf_a1b2c3d4e5f6",
        "expectedVersion": 1,
        "transition": "run_lint"
      },
      "inputSchema": { "type": "object" }
    }
  ],
  "evidence": []
}

Each link is a pre-filled praxec.command submit call — the model copies args verbatim. The link rel is the transition name (run_lint).

If the initial state has actor: deterministic transitions, the runtime chains through them automatically and returns the first state that needs a decision (the chain steps appear under a chain array when any fired).

runId uniqueness: supply runId to assert uniqueness. If an instance with that runId already exists, the call returns RUN_ID_ALREADY_RUNNING (see error shapes below) rather than creating a duplicate. Omit runId to let the runtime mint one.


submit

Advance a workflow by firing a transition.

Example request:

{
  "name": "praxec.command",
  "arguments": {
    "workflowId": "wf_a1b2c3d4e5f6",
    "expectedVersion": 4,
    "transition": "deploy",
    "arguments": {
      "confirm": true
    }
  }
}

Example response (success):

{
  "workflow": {
    "id": "wf_a1b2c3d4e5f6",
    "definitionId": "deploy_pipeline",
    "definitionVersion": 1,
    "state": "deployed",
    "version": 5
  },
  "result": { "status": "completed" },
  "context": {
    "lintPassed": true,
    "testsPassed": true,
    "coverage": 87.5,
    "artifactId": "art_xyz789",
    "deploymentId": "dep_abc123"
  },
  "links": [],
  "evidence": []
}

There is no top-level terminal flag. A workflow is terminal when result.status is "completed" and links is empty — the runtime forces result.status to completed for any terminal state. A non-terminal submit returns result.status "executed" (and a non-empty links array of legal next transitions).

Submit error codes:

Code Meaning
STALE_WORKFLOW_VERSION expectedVersion doesn’t match. Fetch current state with praxec.query({workflowId}) and retry.
ACTOR_MISMATCH Transition requires a human principal but submitter is not human.
GUARD_REJECTED One or more guards rejected the transition.
INPUT_SCHEMA_VIOLATION Arguments don’t match the transition’s input schema.
INVALID_TRANSITION Transition doesn’t exist or isn’t legal from the current state.
EXECUTOR_FAILED Executor ran but failed.

define

Write a lexicon entry. Requires with_lexicon_writes(true) on the server (default off in production deployments).

Example request:

{
  "name": "praxec.command",
  "arguments": {
    "subject": "lexicon:evidence-pack",
    "definition": {
      "definition_short": "A structured set of artefacts that justify a decision.",
      "boundedContext": "swe-agent",
      "refs": ["evidence-packs", "evidence pack"],
      "governance": "human-only"
    }
  }
}

The definition body reads four fields: definition_short (required), boundedContext (camelCase on input), refs, and governance. Synonyms are carried as refs. Any other keys — including a definition_long or aliases — are ignored.

Example response:

{
  "term": "evidence-pack",
  "entry": {
    "definition_short": "A structured set of artefacts that justify a decision.",
    "bounded_context": "swe-agent",
    "refs": ["evidence-packs", "evidence pack"],
    "governance": "human-only"
  },
  "persisted_to": "overlay"
}

Note the echoed entry stores bounded_context in snake_case — the dispatcher reshapes the camelCase input key on the way in.

Emits a lexicon.defined audit event on success. Defines land in a gateway-lifetime in-memory overlay. Persist them by editing praxec.yaml and reloading.

Define error codes:

Code Meaning
LEXICON_DEFINE_REQUIRES_HUMAN Caller is an agent; term governance is human-only. Route through an actor: human transition to commit.
LEXICON_WRITES_DISABLED Server was started without lexicon write support. See operator path in the error’s links[].
INVALID_LEXICON_ENTRY Definition shape is malformed.
LEXICON_ALIAS_COLLISION An alias collides with another entry’s term or alias in the same bounded context.

Error response shape

Ambiguous argument combinations return a structured error — never an MCP protocol error:

AMBIGUOUS_INTENT

Fired when the args don’t match any known dispatch shape (e.g., conflicting dispatch keys are present). The praxec.command variant returns three example links — one per command shape:

{
  "error": {
    "code": "AMBIGUOUS_INTENT",
    "message": "praxec.command args do not match a known dispatch shape",
    "hint": "see §32 dispatch table: start (definitionId only), submit (workflowId+expectedVersion+transition), define (subject namespaced + definition)"
  },
  "links": [
    { "rel": "start_example",  "method": "praxec.command", "args": { "definitionId": "<your-workflow>" } },
    { "rel": "submit_example", "method": "praxec.command", "args": { "workflowId": "<id>", "expectedVersion": 0, "transition": "<name>" } },
    { "rel": "define_example", "method": "praxec.command", "args": { "subject": "lexicon:<term>", "definition": { "definition_short": "..." } } }
  ]
}

The praxec.query variant carries the message "praxec.query args do not match a known dispatch shape" and two links — { rel: "home" } and { rel: "search" }.

LEXICON_WRITES_DISABLED

Fired when define is called on a server with lexicon writes off.

{
  "error": {
    "code": "LEXICON_WRITES_DISABLED",
    "message": "This runtime does not accept lexicon define commands.",
    "hint": "Operators add lexicon terms via the `praxec lexicon define` CLI subcommand."
  },
  "links": [
    { "rel": "operator_path", "method": "cli",            "args": { "command": "praxec lexicon define <term> <definition>" } },
    { "rel": "lookup",        "method": "praxec.query", "args": { "subject": "lexicon:<term>" } }
  ]
}

The cli rel is informational — it names the operator-facing alternative, not a tool the model can call. The lookup rel keeps the read path discoverable.

RUN_ID_ALREADY_RUNNING

Fired when a start command supplies a run_id that already exists.

{
  "error": {
    "code": "RUN_ID_ALREADY_RUNNING",
    "message": "An instance already exists with run_id 'r-abc123'.",
    "hint": "Each run_id is single-use. Fetch the existing instance with the linked get, or retry with a fresh run_id."
  },
  "links": [
    { "rel": "get", "method": "praxec.query", "args": { "workflowId": "<existing>" } }
  ]
}

SUBJECT_NEEDS_DEFINITION

Fired when a workflow start (or mid-workflow transition) encounters a subject that has no lexicon entry. The runtime pauses the command and returns a structured interaction instead of executing:

{
  "interaction": {
    "kind": "SUBJECT_NEEDS_DEFINITION",
    "unknown_subject": "evidence-foo",
    "context": {
      "encountered_in": "workflow:swe_agent state:retrieving",
      "bounded_context": "swe-agent"
    },
    "candidates": [
      { "term": "evidence-pack", "distance": 2, "match_kind": "fuzzy_close",
        "definition_preview": "A bundle of facts the editor uses…" }
    ]
  },
  "queued_command": {
    "method": "praxec.command",
    "args": { }
  },
  "links": [
    {
      "rel": "link_as_alias",
      "method": "praxec.command",
      "args": { "subject": "lexicon:evidence-pack", "definition": { "aliases_add": ["evidence-foo"] } },
      "hint": "Use this if 'evidence-foo' is a synonym for 'evidence-pack'."
    },
    {
      "rel": "define_new",
      "method": "praxec.command",
      "args": { "subject": "lexicon:evidence-foo", "definition": { "definition_short": "<fill in>", "boundedContext": "swe-agent" } },
      "hint": "Use this if 'evidence-foo' is a genuinely new concept."
    },
    {
      "rel": "cancel",
      "method": "praxec.command",
      "args": { "intent": "cancel_pending_subject", "unknown_subject": "evidence-foo" },
      "hint": "Abandon the original command — the subject was a mistake."
    }
  ]
}

Follow one link. Resolution updates the live lexicon (or drops the placeholder for cancel). The client retries the original command. See Lexicon for the full PENDING_DEFINITION / SUBJECT_NEEDS_DEFINITION flow.


HATEOAS contract

Every response — success or error — carries a links[] array:

{ "rel": "string", "method": "praxec.query" | "praxec.command", "args": { ... } }

The args object is pre-filled with the exact shape the next operation needs. Models chain by copying link.args verbatim — they never derive the next call from the schema. Static schema declares what’s valid; dynamic links[] declares what’s valid right now.