Added
- New domain
tools/webhooks.py— V1 inbound-webhook read surface:list_webhooks
(filter by name/source),get_webhook(by name). Read-only on purpose — create/update
/delete carry credential material (BasicAuth/APIKeyAuth/HMACAuth) kept out of MCP. - New domain
tools/meta.py— server-side aggregation over existing reads:
top_failing_workflows(rank workflows by FAILED count in a window),
list_stuck_runs(RUNNING runs older than a minute threshold),describe_run_failure
(one-call diagnostic that runsget_run→ logs/events/timings in parallel via
asyncio.gatherand assembles a single structured payload). - New SDK-gap read tools in existing domains:
get_workflow_version,
get_workflow_metrics,get_task_stats,get_scheduled(closes the
get_cronasymmetry),list_dag_tasks. - New SDK-gap mutating tools:
cancel_run/replay_run(single-ID, no
dry-run/cap dance — for many IDs use the bulk variants),replay_events(500-event
cap),bulk_delete_scheduled(dry-run default, mutex IDs/filter, 500-cap on
explicit IDs). HatchetAPIErrortyped exception —RuntimeErrorsubclass withstatus: int | None
andkind: Literal["validation_error" | "unauthorized" | "not_found" | "conflict" | "rate_limited" | "server_error" | "unknown"]. Message prefix"Hatchet API error"is
preserved so existingpytest.raises(RuntimeError, match=...)callers still match
(src/hatchet_mcp/_shared.py).- MCP read-tool annotations — every read tool now advertises
ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=True)so clients (Cursor, Claude Code in trusted-server mode) can skip
per-call approval prompts for reads. Every registered tool (read + mutating) also carries
the MCP 2025-06-18titlefield, derived from the snake_case name
(src/hatchet_mcp/server.py,src/hatchet_mcp/_shared.py). tool.ok/tool.errorstderr records now include the MCP JSON-RPCrequest_id
(present, may benulloutside a request scope);tool.erroralso carrieserror_status
(int) anderror_kind(str) fromHatchetAPIError. Concurrent stdio tool calls become
attributable in stderr (src/hatchet_mcp/_shared.py,src/hatchet_mcp/_logging.py).- Progress notifications on bulk ops —
cancel_runs/replay_runs/
bulk_delete_scheduledaccept an injectedctx: Context | None, emittingctx.infoand
ctx.report_progressat resolve / submit / done milestones; existing callers (noctx)
see no behavior change (src/hatchet_mcp/tools/runs.py,
src/hatchet_mcp/tools/schedules.py).
Changed (breaking)
get_run_statusreturn-key flips from{"workflow_run_id": "..."}to
{"workflowRunId": "..."}, restoring CLAUDE.md invariant 4 ("tool output uses
the Hatchet REST shape — camelCase viaby_alias=True"). The resource at
hatchet://runs/{workflow_run_id}/statusfollows by pass-through. Path-parameter names
are unchanged. Callers parsing the response key by name must switch toworkflowRunId
(src/hatchet_mcp/tools/runs.py).list_runsnow defaults to a 9-field projection (minimal_output=True):
taskExternalId,workflowRunExternalId,status,workflowName,startedAt,
finishedAt,errorMessage,parentTaskExternalId,numSpawnedChildren—
typically ~5-7x smaller per response (a 124 KB / 100-row scan drops to ~20 KB).
Setminimal_output=Falseto get every field, or useget_runfor one run's full
record. Follows the GitHub MCP convention of defaulting list tools to a compact
shape (src/hatchet_mcp/tools/runs.py).list_eventsnow defaults tominimal_output=True, which drops each event's
payload,triggeredRuns, andadditionalMetadatafields. Set
minimal_output=Falsefor the full record, or useget_eventfor one event's full
payload. Same convention aslist_runs(src/hatchet_mcp/tools/events.py).
Callers that depended on the full row from list_runs / list_events, or on the
snake_case get_run_status key, must update.
Changed
- HTTP connection reuse —
_rest_callnow invokes a process-wide cachedApiClient
(client.get_api_client(), guarded bythreading.Lockfor the double-checked init under
asyncio.to_threadworkers) instead of constructing a freshApiClient→RESTClientObject
→urllib3.PoolManageron every call. Keep-alive now amortizes TCP+TLS across the 11
REST-direct sites (events, observability, tasks, runs filter-resolve, etc.). The SDK's own
aio_*feature-client methods retain the per-call construction pattern upstream
(src/hatchet_mcp/client.py,src/hatchet_mcp/_shared.py). describe_run_failureruns its three independent sub-reads (logs / events / timings)
viaasyncio.gather(return_exceptions=True). The timings branch is best-effort — an
ApiException/HatchetAPIError/asyncio.TimeoutErrordemotes it totimings=null
while logs + events still succeed (src/hatchet_mcp/tools/meta.py).get_run_timingsdescription now explicitly calls out that it is the cheapest way
to expand a parent run's child task tree in a single call (usedepth=1for direct
children) — closes a discovery gap that previously forced callers to page through
list_runsto find children of a known parent
(src/hatchet_mcp/tools/observability.py).
Fixed
- MCP resources bypassed the reliability wrapper (latent since v0.2.0). Resource handlers
inresources.pyimported raw tool functions and awaited them directly, skipping the 30s
per-call deadline, retry on 5xx/429/transport, andtool.ok/tool.errorlog records — a
hung Hatchet response could hang the MCP session with no audit trail. Resources now go
through pre-wrapped handlers built once at module import time
(src/hatchet_mcp/resources.py). - Concurrency race on
get_api_client()init closed viathreading.Lock+ double-check
(twoasyncio.to_threadworkers could otherwise both see_api_client is Noneand build
twoApiClientinstances) (src/hatchet_mcp/client.py).
Docs
- New spec
v0.3.0/operational-toolkit-expansionand paired ADR — 7 decisions, AC-1
through AC-7 (docs/roadmap/v0.3.0/,docs/design/v0.3.0/). docs/roadmap/README.mdactive-spec index updated with the v0.3.0 row.