# Hitl Mcp

> A production-ready, state-persistent MCP server for Human-in-the-Loop (HITL) interaction. Enables AI agents to suspend execution, ask structured questions to users, and resume contextually across sessions — friendly with ACP.

- **Type:** MCP server
- **Install:** `agentstack add mcp-zenlixai-hitl-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ZenlixAI](https://agentstack.voostack.com/s/zenlixai)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ZenlixAI](https://github.com/ZenlixAI)
- **Source:** https://github.com/ZenlixAI/hitl-mcp

## Install

```sh
agentstack add mcp-zenlixai-hitl-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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:

1. The Agent and the application do not share a stable model for "pending human input".
2. Human answers are hard to correlate with the exact Agent run and session that requested them.
3. 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_identity` and `agent_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`, and `extra`
- for `single_choice`, each option may also declare `followup_fields`
- an optional `default_answer`
- a status such as `pending`, `answered`, `skipped`, or `cancelled`

Supported question types:

- `single_choice`
- `multi_choice`
- `text`
- `boolean`
- `range`

### Caller scope

Every operation is scoped by:

- `agent_identity`
- `agent_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_choice` option declares `followup_fields`, callers may send `{ value, fields }`
- required `followup_fields` are 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 `memory` mode, this works for single-process deployments via an in-process timeout worker
- in `redis` mode, this works across instances via Redis-backed timeout processing

The derived defaults are:

- `single_choice`: first option value
- `multi_choice`: first option value as a single-element array
- `boolean`: `true`
- `range`: `range_constraints.min`
- `text`: `"none"`

Timeout-generated answers keep the normal `answered` status and add metadata such as:

- `auto_response_at`
- `is_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 left
- `progressive`: 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

```bash
git clone 
cd hitl-mcp
npm install
```

## Run in development

```bash
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:

```bash
docker build -t hitl-mcp .
```

Run with in-memory storage:

```bash
docker run --rm -p 3000:3000 \
  -e MCP_URL=http://localhost:3000 \
  hitl-mcp
```

Run with Redis:

```bash
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

```bash
export MCP_URL=http://localhost:3000
npm run dev
```

## Minimal create request

```bash
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

1. The Agent creates one or more questions in its caller scope.
2. The Agent immediately calls `hitl_wait`.
3. A UI or backend fetches pending questions for the same caller scope.
4. A human answers one or more pending questions.
5. The Agent waits on the scope.
6. When the scope has no pending questions left, wait returns a terminal result.

### Sequence 2: partial submission

1. The Agent creates multiple questions.
2. The human answers only a subset.
3. The server persists those answers and keeps the remaining questions pending.
4. The Agent can keep waiting.
5. Additional submissions continue until the scope is complete.

### Sequence 3: progressive wait

1. `HITL_WAIT_MODE=progressive`
2. The Agent calls `hitl_wait`.
3. Any answer, skip, or cancellation in the scope wakes the waiter.
4. The wait result reports which `question_id`s changed.
5. The Agent decides whether to continue waiting or act on the intermediate update.

### Sequence 4: cancellation

1. A caller cancels one question or all pending questions in the scope.
2. The server updates scope state and notifies waiters.
3. If no pending questions remain, the scope becomes terminal.

### Sequence 5: timeout auto-response

1. The Agent creates questions with explicit or implicit timeout behavior.
2. In Redis mode, the server schedules the pending group for timeout processing.
3. If no human response arrives before the deadline, a Redis-backed timeout worker auto-answers remaining pending questions.
4. The worker publishes a scope update so waiters on any instance can wake up.
5. `hitl_wait` returns 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 be `hitl_wait`
- do not treat `hitl_ask` as completion
- do not substitute `hitl_get_pending_questions` for 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:

```json
{
  "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_id` must not be provided by the caller
- the server generates `question_id`
- one request can create multiple questions
- explicit `default_answer` is recommended
- if `default_answer` is omitted, the server derives one
- if `timeout_seconds` is omitted, the server uses the configured default timeout

### `hitl_wait`

Waits on the current caller scope.

Typical response fields:

- `status`
- `is_terminal`
- `changed_question_ids`
- `pending_questions`
- `resolved_questions`
- `answered_question_ids`
- `skipped_question_ids`
- `cancelled_question_ids`
- `is_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_at`
- `is_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:

```json
{
  "answers": {
    "q_01JXYZ...": { "value": true }
  },
  "skipped_question_ids": ["q_01JABC..."],
  "idempotency_key": "idem-1"
}
```

Notes:

- `answers` may 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:

```json
{
  "question_ids": ["q_01JXYZ..."],
  "reason": "no longer needed"
}
```

Or:

```json
{
  "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:

```json
{
  "request_id": "http-request-id",
  "success": true,
  "data": {},
  "error": null
}
```

On failure:

```json
{
  "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-identity` on every caller-scoped request

Operationally:

- the server reads `agent_identity` directly from `x-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:

```json
{
  "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_choice` with `options`
- `single_choice.options[].followup_fields` for option-specific text follow-up inputs
- `multi_choice` with `options`
- `text` with optional `text_constraints`
- `boolean`
- `range` with `range_constraints`

Additional create-time fields:

- `timeout_seconds`: optional positive integer timeout for the group
- `default_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:

```json
{
  "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_FOUND`
- `ANSWER_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](https://github.com/ZenlixAI)
- **Source:** [ZenlixAI/hitl-mcp](https://github.com/ZenlixAI/hitl-mcp)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-zenlixai-hitl-mcp
- Seller: https://agentstack.voostack.com/s/zenlixai
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
