agent-graph-mcp 0.3.0

Run 9 agents at once — MCP server for graph-orchestrated LLM workflows with parallel fan-out (up to 16 nodes), checkpoint/resume, HITL approvals, HMAC receipts
Documentation
# Graph operations MCP tools design

These 12 tools use the existing rmcp convention:

```rust
#[tool(description = "...")]
fn tool_name(
    &self,
    Parameters(params): Parameters<ToolParams>,
) -> Result<Json<StructuredOutput>, ErrorData>
```

All responses use the existing `StructuredOutput` envelope. Successful `data` payloads are described below; failures should use stable `error_code` values and retain the envelope's `ok: false` shape.

## Shared typed enums

These enums are intentionally string-compatible in the wire schema while preventing invalid values in Rust. Add `serde`'s `rename_all = "snake_case"` and `schemars::JsonSchema` derives.

```rust
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalDecision {
    Approve,
    Reject,
    RequestChanges,
}

#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RenderFormat {
    Mermaid,
    Json,
}
```

## 1. `graph_approval_list`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphApprovalListParams {
    /// Optional run ID; when omitted, list approvals across all retained runs.
    #[serde(default)]
    pub run_id: Option<String>,
    /// Optional status filter. Defaults to `pending`.
    #[serde(default)]
    pub status: Option<String>,
    /// Maximum number of records, default 100 and hard-capped by the server.
    #[serde(default)]
    pub limit: Option<u32>,
    /// Pagination cursor returned by the previous call.
    #[serde(default)]
    pub cursor: Option<String>,
}
```

Description: `List human-approval requests, optionally filtered by run ID and status, with bounded pagination.`

Implementation: query `PersistentStore`'s `approval_requests` table when configured; otherwise use an approval index owned by `RunManager` (the current runtime has no approval index, so add one). Return `data: { approvals: [...], next_cursor, truncated }`, including approval ID, run ID, node ID, audience, prompt, allowed decisions, status, expiry, and decision metadata. Do not expose unrelated run state.

## 2. `graph_approval_get`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphApprovalGetParams {
    /// Approval request identifier.
    pub approval_id: String,
}
```

Description: `Fetch one approval request by approval_id, including its current status and decision metadata.`

Implementation: read the approval row from `PersistentStore`; volatile mode should look up the run manager's approval registry. Return `data: { approval: {...} }`. Missing IDs should return `APPROVAL_NOT_FOUND`; expired pending requests should be reported as `expired`, not silently converted to rejection.

## 3. `graph_approval_decide`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphApprovalDecideParams {
    /// Approval request identifier.
    pub approval_id: String,
    /// Decision to apply: approve, reject, or request_changes.
    pub decision: ApprovalDecision,
    /// Human-readable rationale or requested changes.
    #[serde(default)]
    pub decision_note: Option<String>,
    /// Stable caller/principal identity recorded in the audit trail.
    pub decided_by: String,
    /// Optional idempotency key for safe retries.
    #[serde(default)]
    pub idempotency_key: Option<String>,
}
```

Description: `Record an approval decision atomically and resume or terminate the associated run according to the decision.`

Implementation: validate the approval is pending, unexpired, and that the decision is in `allowed_decisions`; atomically update the store row with decision, note, decided_by, and timestamp. Then signal the run's approval/resume mechanism. `approve` resumes, `reject` terminates, and `request_changes` returns the run to a waiting/rework state. Use `APPROVAL_ALREADY_DECIDED`, `APPROVAL_EXPIRED`, and `APPROVAL_DECISION_NOT_ALLOWED` as stable errors. Persist before signaling to make retries restart-safe.

## 4. `graph_run_start`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunStartParams {
    /// Registered graph ID/name.
    pub graph_id: String,
    /// Input passed to the graph entry node.
    #[serde(default)]
    pub input: Option<Value>,
    /// Optional exact graph topology/version digest to pin.
    #[serde(default)]
    pub graph_version: Option<String>,
    /// Optional caller correlation/thread identifier.
    #[serde(default)]
    pub thread_id: Option<String>,
    /// Optional idempotency key; retries return the original run.
    #[serde(default)]
    pub idempotency_key: Option<String>,
}
```

Description: `Start asynchronous execution of a registered graph and return its run_id without waiting for completion.`

Implementation: resolve and version-check `self.graphs`, enforce input limits, check `store` idempotency, allocate/admit in `self.runs`, persist an execution row, and call `RunManager::start`. Return `data: { run_id, status: "accepted", graph_id, graph_version, thread_id }` immediately. Capacity failure must not leave an accepted orphan run.

## 5. `graph_run_wait`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunWaitParams {
    /// Run identifier.
    pub run_id: String,
    /// Maximum wait in milliseconds; default 30_000, hard-capped by policy.
    #[serde(default)]
    pub timeout_ms: Option<u64>,
    /// Poll interval in milliseconds; server may clamp this value.
    #[serde(default)]
    pub poll_interval_ms: Option<u64>,
}
```

Description: `Wait until a run reaches a terminal state or the bounded timeout expires.`

Implementation: repeatedly read `self.runs.get(run_id)` (and durable execution state if enabled) without holding the outer mutex across sleeps. Terminal states are `completed`, `failed`, and `cancelled`; timeout returns `ok: true` with `status: "timeout"` and current run data, not a false terminal result. Return `data: { run, terminal, timed_out }`.

## 6. `graph_run_cancel`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunCancelParams {
    /// Run identifier.
    pub run_id: String,
    /// Why cancellation was requested; retained in the audit/event record.
    pub reason: String,
    /// Optional idempotency key for repeated cancellation requests.
    #[serde(default)]
    pub idempotency_key: Option<String>,
}
```

Description: `Request cooperative cancellation of a running graph execution.`

Implementation: call `self.runs.cancel`, which sets the cancellation flag observed at node boundaries; append a cancellation-request event and update durable execution status if configured. Repeated calls are idempotent. Return `data: { run_id, status: "cancellation_requested", reason, effective_at_boundary: true }`. Reject terminal runs with `RUN_ALREADY_TERMINAL` or return their unchanged terminal status consistently.

## 7. `graph_run_get`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunGetParams {
    /// Run identifier.
    pub run_id: String,
    /// Include the full state/final output when true; default false for bounded status reads.
    #[serde(default)]
    pub include_state: Option<bool>,
    /// Include approval records associated with this run; default true.
    #[serde(default)]
    pub include_approvals: Option<bool>,
}
```

Description: `Get current run status, resource/budget usage, and pending approvals.`

Implementation: read `RunManager::get`; merge durable execution/checkpoint counters where available. Derive budget usage from recorded node/attempt/token counters (or return explicit `unknown` fields rather than inventing values). Include `pending_approvals` from the approval store/index. Return `data: { run_id, status, success, graph_id, graph_version, started_at, finished_at, usage, pending_approvals, state?, final_state? }`.

## 8. `graph_run_state`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunStateParams {
    /// Run identifier.
    pub run_id: String,
    /// Optional JSON-pointer projection, e.g. `/user/result`.
    #[serde(default)]
    pub path: Option<String>,
    /// If true, include state history/checkpoint metadata when available.
    #[serde(default)]
    pub include_history: Option<bool>,
}
```

Description: `Read the current state projection for a run, optionally selecting a JSON-pointer path.`

Implementation: use the live `RunRecord.state`; if absent in memory, load the latest durable checkpoint/state projection from `PersistentStore`. Apply RFC 6901 JSON Pointer and return `STATE_PATH_NOT_FOUND` for an invalid path. Return `data: { run_id, cursor/state_version, value, path, history? }`; redact secrets using the existing evidence redaction helper.

## 9. `graph_run_events`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunEventsParams {
    /// Run identifier.
    pub run_id: String,
    /// Inclusive event cursor from which to replay.
    #[serde(default)]
    pub cursor: Option<u64>,
    /// Maximum events, default 100 and hard-capped at 200.
    #[serde(default)]
    pub limit: Option<u32>,
    /// Wait briefly for events when the cursor is at the live tail.
    #[serde(default)]
    pub wait_ms: Option<u64>,
}
```

Description: `Replay run events from an inclusive cursor with restart-safe pagination and gap detection.`

Implementation: use `RunManager::events` for volatile records; use the durable `events(run_id, seq)` table when configured, preferring durable records after restart. Preserve `next_cursor`, `gap`, `truncated`, and `dropped` semantics. Return `data: { run_id, events, next_cursor, gap, truncated, dropped }`; never reuse a cursor after compaction without reporting `gap: true`.

## 10. `graph_run_receipt`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRunReceiptParams {
    /// Run identifier.
    pub run_id: String,
    /// If true, include the evidence bundle/artifact reference as well as the receipt.
    #[serde(default)]
    pub include_bundle: Option<bool>,
    /// Optional digest expected by the caller for integrity verification.
    #[serde(default)]
    pub expected_digest: Option<String>,
}
```

Description: `Fetch the canonical execution receipt for a run and optionally verify its evidence bundle digest.`

Implementation: read `RunRecord.receipt` or the durable receipt artifact. Compare `expected_digest` against the canonical serialized receipt/bundle digest; mismatch returns `RECEIPT_DIGEST_MISMATCH`. Return `data: { receipt, bundle?, integrity: { verified, digest } }`; missing receipts for nonterminal runs should be `RECEIPT_NOT_READY`, not an empty success.

## 11. `graph_policy_check`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphPolicyCheckParams {
    /// Graph ID to check, unless an inline spec is supplied.
    #[serde(default)]
    pub graph_id: Option<String>,
    /// Inline graph spec for pre-registration checks.
    #[serde(default)]
    pub spec: Option<Value>,
    /// Optional policy profile name; defaults to the server's baseline policy.
    #[serde(default)]
    pub policy_profile: Option<String>,
    /// Optional caller-provided policy overrides, subject to server allowlisting.
    #[serde(default)]
    pub policy: Option<Value>,
}
```

Description: `Run a fail-closed preflight policy check against a registered graph or inline graph specification.`

Implementation: resolve exactly one source (`graph_id` xor `spec`), call `parse_and_validate`, then evaluate node types, graph size/iterations/parallelism, model allowlists, prompt/input limits, approval requirements, and unsupported capabilities. Return `data: { decision: "allow"|"deny"|"review", graph_id?, graph_version?, violations, warnings, effective_policy }`. A missing policy profile or unknown override must deny rather than silently weaken policy.

## 12. `graph_render`

```rust
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GraphRenderParams {
    /// Registered graph ID/name, unless an inline spec is supplied.
    #[serde(default)]
    pub graph_id: Option<String>,
    /// Inline graph spec to render without registration.
    #[serde(default)]
    pub spec: Option<Value>,
    /// Output representation: `mermaid` or `json`.
    pub format: RenderFormat,
    /// Include node prompts/configuration in JSON output; default false.
    #[serde(default)]
    pub include_details: Option<bool>,
}
```

Description: `Render a registered or inline graph as Mermaid or a normalized JSON topology.`

Implementation: resolve one source, validate inline specs through `parse_and_validate`, and use the existing `AgentGraphServer::mermaid` helper for Mermaid. JSON should expose only normalized topology by default: `{ graph_id, graph_version, entry, nodes, edges, topology_hash }`; add prompt/config details only when requested and redact secrets. Return `RENDER_SOURCE_REQUIRED` when neither source is supplied and `RENDER_SOURCE_AMBIGUOUS` when both are supplied.

## Cross-cutting implementation notes

- Add all parameter structs and enums to `src/tools.rs`; import `serde::{Deserialize, Serialize}` as needed and keep `Value` for extensible policy/graph payloads.
- Register methods inside the existing `#[tool_router] impl AgentGraphServer`; each method returns `Result<Json<StructuredOutput>, ErrorData>`.
- Validate IDs, enum values, body sizes, and limits before acquiring long-lived locks.
- Prefer durable store reads when `self.store.is_some()`, but keep volatile mode explicit in response metadata.
- Approval lifecycle requires a real in-memory approval index and run wake/resume primitive; the existing schema alone does not provide those semantics.
- Current `RunManager` has status/state/events/receipt/cancel primitives, but no timestamps, usage counters, durable receipt loading, or wait notification. Add those as implementation prerequisites rather than fabricating fields in tool responses.