Install
$ agentstack add mcp-zenlixai-hitl-mcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
hitl-mcp
Question-only HITL MCP server for agent workflows.
[](https://www.typescriptlang.org/) [](https://modelcontextprotocol.io/) [](LICENSE)
[English](README.md) | [中文](README-zh.md)
Table of Contents
- [Background](#background)
- [What hitl-mcp is](#what-hitl-mcp-is)
- [Goals](#goals)
- [Non-goals](#non-goals)
- [Core model](#core-model)
- [Quick start](#quick-start)
- [How interaction works](#how-interaction-works)
- [MCP tools](#mcp-tools)
- [HTTP API](#http-api)
- [Runtime configuration](#runtime-configuration)
- [How it works internally](#how-it-works-internally)
- [Architecture](#architecture)
- [Operations](#operations)
- [Project structure](#project-structure)
Background
In many Agent systems, the Agent eventually reaches a point where it cannot continue autonomously:
- a human must approve or reject a decision
- a human must choose one option from several candidates
- a human must provide missing business input
- a workflow must pause until manual confirmation arrives
Without a dedicated HITL layer, these workflows are usually implemented with ad hoc prompts, side-channel UIs, or custom callback protocols. That creates three recurring problems:
- The Agent and the application do not share a stable model for "pending human input".
- Human answers are hard to correlate with the exact Agent run and session that requested them.
- Waiting, partial submission, cancellation, and recovery semantics become inconsistent across clients.
hitl-mcp solves this by exposing a narrow, explicit contract for human questions:
- Agents create questions
- clients or operator UIs read pending questions
- humans answer, skip, or cancel them
- Agents wait on a caller-scoped state machine until work is complete
What hitl-mcp is
hitl-mcp is a question-oriented human-in-the-loop server built on MCP and HTTP.
It provides two access surfaces over the same underlying state:
- MCP tools for Agents and Agent platforms
- HTTP APIs for operator consoles, backends, or custom approval UIs
The public abstraction is intentionally small:
- the public unit is
question - questions belong to a caller scope
- answers may be submitted incrementally
- waiting is modeled as a scope-level operation, not a question-level long poll
The server is designed for workflows where the Agent is the initiator, but a human completes part of the decision loop.
Goals
- Provide a stable, minimal HITL contract for Agent workflows.
- Make caller isolation explicit through
agent_identityandagent_session_id. - Support multiple pending questions in the same caller scope.
- Support partial progress instead of forcing an all-at-once final submission.
- Allow MCP and HTTP clients to operate on the same underlying question state.
- Keep storage pluggable so local development can use memory and production can use Redis.
- Keep operational behavior observable through health, readiness, metrics, and structured logs.
Non-goals
- Not a generic workflow engine.
- Not a UI framework for approval consoles.
- Not a task queue or event bus.
- Not a general-purpose form builder.
- Not a policy engine for access control and reviewer assignment.
- Not a durable orchestration platform for arbitrary business processes.
hitl-mcp only manages question state, caller scoping, waiting semantics, and answer submission.
Core model
Public unit: question
Externally, the system only exposes question.
A question has:
- a server-generated
question_id - a
type - prompt metadata such as
title,description,tags, andextra - for
single_choice, each option may also declarefollowup_fields - an optional
default_answer - a status such as
pending,answered,skipped, orcancelled
Supported question types:
single_choicemulti_choicetextbooleanrange
Caller scope
Every operation is scoped by:
agent_identityagent_session_id
This scope is the isolation boundary for:
- creating questions
- listing pending questions
- waiting for progress
- submitting answers
- cancelling questions
Two Agents can ask identical questions without colliding as long as their caller scopes differ.
Internal groups vs public API
The storage layer may still use grouping internally, but grouping is an implementation detail.
The public API is intentionally question-first:
- create questions
- fetch pending questions
- submit answers by
question_id - cancel by
question_id - wait on the scope
Partial submission
Answers do not need to arrive in one batch.
The server accepts incremental progress:
- answer one question now
- answer another question later
- skip optional questions explicitly
- continue waiting until the scope becomes complete
Answer payloads remain backward compatible:
- existing callers may continue sending
{ value } - when a selected
single_choiceoption declaresfollowup_fields, callers may send{ value, fields } - required
followup_fieldsare validated against the selected option
Timeout auto-response
Question groups may also define timeout behavior at creation time.
- callers may set
timeout_seconds - if omitted, the server uses
HITL_PENDING_DEFAULT_TIMEOUT_SECONDS - each question may set
default_answer - if omitted, the server derives one by question type
Timed-out pending questions are automatically answered with their resolved default answers.
- in
memorymode, this works for single-process deployments via an in-process timeout worker - in
redismode, this works across instances via Redis-backed timeout processing
The derived defaults are:
single_choice: first option valuemulti_choice: first option value as a single-element arrayboolean:truerange:range_constraints.mintext:"none"
Timeout-generated answers keep the normal answered status and add metadata such as:
auto_response_atis_timeout_auto_response
Wait modes
hitl_wait and the equivalent scope-level wait behavior support two modes:
terminal_only: return only when the scope has no pending questions leftprogressive: return after every state change, then let the caller wait again
terminal_only is simpler for linear workflows. progressive is better when the caller needs to react to each intermediate update.
When hitl_wait is called through MCP, it also emits a progress notification every 30 seconds while the wait is still pending.
Quick start
Installation
git clone
cd hitl-mcp
npm install
Run in development
npm run dev
Default local bind:
- HTTP base URL:
http://0.0.0.0:3000 - MCP base URL:
http://0.0.0.0:3000/mcp - HTTP API prefix:
/api/v1
Run with Docker
Build the image:
docker build -t hitl-mcp .
Run with in-memory storage:
docker run --rm -p 3000:3000 \
-e MCP_URL=http://localhost:3000 \
hitl-mcp
Run with Redis:
docker run --rm -p 3000:3000 \
-e MCP_URL=http://localhost:3000 \
-e HITL_STORAGE=redis \
-e HITL_REDIS_URL=redis://host.docker.internal:6379 \
hitl-mcp
Run from source with environment variables
export MCP_URL=http://localhost:3000
npm run dev
Minimal create request
curl -X POST "http://localhost:3000/api/v1/questions" \
-H "Content-Type: application/json" \
-H "x-agent-identity: agent/example" \
-H "x-agent-session-id: session-123" \
-d '{
"title": "Outline confirmation",
"questions": [
{
"type": "single_choice",
"title": "Confirm the current outline",
"options": [
{ "value": "approved", "label": "Approved" },
{
"value": "revise",
"label": "Revise",
"followup_fields": [
{
"id": "comment",
"type": "text",
"label": "Revision comment",
"required": true,
"description": "Explain what should change."
}
]
}
]
}
]
}'
How interaction works
This section describes the intended runtime flow, regardless of whether the caller uses MCP tools or HTTP APIs.
Sequence 1: standard ask -> wait -> answer -> complete
- The Agent creates one or more questions in its caller scope.
- The Agent immediately calls
hitl_wait. - A UI or backend fetches pending questions for the same caller scope.
- A human answers one or more pending questions.
- The Agent waits on the scope.
- When the scope has no pending questions left, wait returns a terminal result.
Sequence 2: partial submission
- The Agent creates multiple questions.
- The human answers only a subset.
- The server persists those answers and keeps the remaining questions pending.
- The Agent can keep waiting.
- Additional submissions continue until the scope is complete.
Sequence 3: progressive wait
HITL_WAIT_MODE=progressive- The Agent calls
hitl_wait. - Any answer, skip, or cancellation in the scope wakes the waiter.
- The wait result reports which
question_ids changed. - The Agent decides whether to continue waiting or act on the intermediate update.
Sequence 4: cancellation
- A caller cancels one question or all pending questions in the scope.
- The server updates scope state and notifies waiters.
- If no pending questions remain, the scope becomes terminal.
Sequence 5: timeout auto-response
- The Agent creates questions with explicit or implicit timeout behavior.
- In Redis mode, the server schedules the pending group for timeout processing.
- If no human response arrives before the deadline, a Redis-backed timeout worker auto-answers remaining pending questions.
- The worker publishes a scope update so waiters on any instance can wake up.
hitl_waitreturns the completed scope snapshot with timeout metadata on affected questions.
Scope semantics
Wait is always a scope-level operation.
This is deliberate:
- one Agent run may have multiple pending questions
- the Agent usually cares whether the workflow can continue
- scope-level wait avoids fragmented per-question synchronization logic
Operational rule:
- after every
hitl_ask, the next HITL tool call must behitl_wait - do not treat
hitl_askas completion - do not substitute
hitl_get_pending_questionsfor the first post-ask wait
MCP tools
hitl-mcp exposes the following MCP tools:
hitl_ask
Creates one or more questions for the current caller scope.
Input shape:
{
"title": "Release decision",
"description": "Human approval required before deploy",
"ttl_seconds": 3600,
"timeout_seconds": 900,
"questions": [
{
"type": "boolean",
"title": "Approve deployment?",
"default_answer": { "value": true }
}
]
}
Notes:
question_idmust not be provided by the caller- the server generates
question_id - one request can create multiple questions
- explicit
default_answeris recommended - if
default_answeris omitted, the server derives one - if
timeout_secondsis omitted, the server uses the configured default timeout
hitl_wait
Waits on the current caller scope.
Typical response fields:
statusis_terminalchanged_question_idspending_questionsresolved_questionsanswered_question_idsskipped_question_idscancelled_question_idsis_complete
resolved_questions contains the full resolved question objects plus their final status and any stored answer.
When a result was produced by timeout automation, the resolved question may also include:
auto_response_atis_timeout_auto_response
When called over MCP, hitl_wait also sends notifications/progress updates every 30 seconds while still waiting.
hitl_get_pending_questions
Returns all pending questions in the current caller scope.
hitl_submit_answers
Submits new answers and optional skips.
Input shape:
{
"answers": {
"q_01JXYZ...": { "value": true }
},
"skipped_question_ids": ["q_01JABC..."],
"idempotency_key": "idem-1"
}
Notes:
answersmay contain any subset of pending questions- optional questions may be skipped explicitly
- required questions cannot be skipped
- submissions accumulate server-side
hitl_cancel_questions
Cancels selected pending questions or all pending questions in the scope.
Input shape:
{
"question_ids": ["q_01JXYZ..."],
"reason": "no longer needed"
}
Or:
{
"cancel_all": true
}
hitl_get_question
Fetches one question by question_id.
HTTP API
The HTTP control plane is primarily for operator UIs, backend services, and troubleshooting.
Response envelope
All HTTP responses use the same envelope:
{
"request_id": "http-request-id",
"success": true,
"data": {},
"error": null
}
On failure:
{
"request_id": "http-request-id",
"success": false,
"data": {},
"error": {
"code": "QUESTION_NOT_FOUND",
"message": "question not found",
"details": null
}
}
Headers and identity rules
For question APIs, the server requires a session header on every caller-scoped request:
- default session header:
x-agent-session-id
Identity rules:
- send
x-agent-identityon every caller-scoped request
Operationally:
- the server reads
agent_identitydirectly fromx-agent-identity
GET /api/v1/healthz
Returns liveness status.
GET /api/v1/readyz
Returns readiness status.
This is the correct probe for Redis-backed production deployments.
GET /api/v1/metrics
Returns an in-process metrics snapshot.
POST /api/v1/questions
Creates one or more questions for the current caller scope.
Request body:
{
"title": "Release decision",
"description": "Human approval required before deploy",
"ttl_seconds": 3600,
"timeout_seconds": 900,
"questions": [
{
"type": "single_choice",
"title": "Confirm the current outline",
"options": [
{ "value": "approved", "label": "Approved" },
{
"value": "revise",
"label": "Revise",
"followup_fields": [
{
"id": "comment",
"type": "text",
"label": "Revision comment",
"required": true,
"description": "Explain what should change."
}
]
}
],
"default_answer": { "value": "approved" }
},
{
"type": "text",
"title": "Anything to note?",
"required": false,
"default_answer": { "value": "none" }
}
]
}
Supported question payloads:
single_choicewithoptionssingle_choice.options[].followup_fieldsfor option-specific text follow-up inputsmulti_choicewithoptionstextwith optionaltext_constraintsbooleanrangewithrange_constraints
Additional create-time fields:
timeout_seconds: optional positive integer timeout for the groupdefault_answer: optional per-question fallback answer used by Redis timeout auto-response
GET /api/v1/questions/pending
Returns all pending questions for the current caller scope.
POST /api/v1/questions/answers
Submits answered and skipped questions.
Request body:
{
"answers": {
"q_01JXYZ...": {
"value": "revise",
"fields": {
"comment": "Please add recovery conditions."
}
}
},
"skipped_question_ids": ["q_01JABC..."],
"idempotency_key": "idem-1"
}
Behavior:
- accepts partial progress
- persists cumulative scope state
- wakes scope waiters after successful submission
- continues to accept plain
{ value }answers when no follow-up fields are required
Typical error codes:
QUESTION_NOT_FOUNDANSWER_VALIDATION_FAILED
POST /api/v1/questions/cancel
Cancels pending questions in the current c
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ZenlixAI
- Source: ZenlixAI/hitl-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.