human-dispatch-mcp
README.md
# human-dispatch-mcp
š **[humandispatch.ai](https://humandispatch.ai)** ā Homepage & provider docs
**A universal dispatch layer for AI-agent-to-human task routing** ā Any business (law firms, VA services, freelancers, agencies) can plug in via webhooks and start receiving AI-dispatched tasks in minutes.
Routes tasks to registered webhook providers with smart matching, fallback chains, and proof-of-completion tracking. Any service provider registers a webhook, and the router matches tasks to providers based on capabilities, region, and budget.
## Quick Start
```bash
# Clone and install
git clone https://github.com/zyntarasystems/human-dispatch-mcp.git
cd human-dispatch-mcp
npm install
# Configure (optional ā works out of the box with manual fallback)
cp .env.example .env
# Build and run
npm run build
node dist/index.js
```
## Testing with MCP Inspector
The easiest way to verify the server is working:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
Open `http://localhost:5173`, enter the proxy session token shown in your terminal, and click **Connect**.
### Test sequence:
1. **List backends** ā call `human_list_backends` to see `webhook_provider` and `manual`
2. **Register a provider** ā call `human_register_provider`:
```json
{
"name": "Test Provider",
"webhook_url": "https://webhook.site/your-uuid",
"webhook_secret": "a-secret-that-is-at-least-32-chars-long!",
"categories": ["digital_micro"],
"task_types": ["digital"],
"regions": ["*"],
"min_budget_usd": 0,
"max_budget_usd": 500,
"max_concurrent_tasks": 10
}
```
3. **Dispatch a task** ā call `human_dispatch_task` with **Raw JSON** input mode:
```json
{
"description": "Test task ā verify the MCP server is routing correctly",
"category": "digital_micro",
"task_type": "digital",
"budget": { "max_usd": 5, "currency": "USD" },
"deadline": {
"complete_by": "2026-04-10T18:00:00Z",
"urgency": "low"
},
"proof_required": ["text_report"],
"quality_sla": "low",
"callback_url": null
}
```
The task should route to your registered provider. If no providers match, it falls through to the manual backend.
## MCP Client Configuration
### Claude Desktop / Cursor / Any MCP Client
```json
{
"mcpServers": {
"human-dispatch": {
"command": "npx",
"args": ["human-dispatch-mcp"]
}
}
}
```
### HTTP Transport
> **Note:** HTTP transport binds to `127.0.0.1` only. For remote access, place a TLS-terminating reverse proxy (e.g. nginx, Caddy) in front of the server. Never expose the port directly.
> **Required:** HTTP transport refuses to start without `MCP_AUTH_TOKEN` set. All `POST /mcp` requests must include `Authorization: Bearer <MCP_AUTH_TOKEN>`. The `/callbacks/task/:taskId` endpoint uses HMAC-signature auth instead ā providers do not see the bearer token.
```json
{
"mcpServers": {
"human-dispatch": {
"command": "npx",
"args": ["human-dispatch-mcp"],
"env": {
"TRANSPORT": "http",
"PORT": "3000",
"MCP_AUTH_TOKEN": "a-long-random-string-32-chars-or-more"
}
}
}
}
```
## Tools Reference
| Tool | Description |
|------|-------------|
| `human_dispatch_task` | Submit a task to be completed by a human worker via the best matching provider |
| `human_get_task_status` | Poll the current status, worker info, and proof submissions for a task |
| `human_cancel_task` | Cancel a pending or in-progress task |
| `human_list_tasks` | List tasks with filters (status, backend, category) and pagination |
| `human_list_backends` | Show available backends, their configuration status, and capabilities |
| `human_register_provider` | Register a webhook provider to receive dispatched tasks |
| `human_list_providers` | List registered providers with stats and filters |
| `human_remove_provider` | Deregister a webhook provider |
## Architecture
```
āāāāāāāāāāāāāāā
ā AI Agent ā
ā (Claude, etc)ā
āāāāāāāā¬āāāāāāāā
ā MCP Protocol (stdio or HTTP)
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā human-dispatch-mcp Server ā
ā ā
ā āāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāā ā
ā ā Task Store ā ā Provider ā ā
ā ā (in-memory)ā ā Registry ā ā
ā āāāāāāāāāāāāāā āāāāāāāāā¬āāāāāāāāā ā
ā ā ā
ā āāāāāāāāāāāāāā āāāāāāāāā¼āāāāāāāāā ā
ā ā Router āāāā Webhook ā ā
ā ā (scoring) ā ā Provider ā ā
ā āāāāāāāā¬āāāāāā ā Adapter ā ā
ā ā āāāāāāāāā¬āāāāāāāāā ā
ā ā ā ā
ā ā āāāāāāāāāāāāā¼āāāāāāāāāā ā
ā ā ā Provider A (law) ā ā
ā ā ā Provider B (VA) ā ā
ā ā ā Provider C (photos) ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāā ā
ā ā¼ ā
ā āāāāāāāāāāāāāā ā
ā ā Manual ā (always-on fallback) ā
ā ā Adapter ā ā
ā āāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
## For Service Providers
Any business can register as a provider to receive AI-dispatched tasks. Here's how:
### 1. Set up a webhook endpoint
Your endpoint receives POST requests with these headers:
| Header | Description |
|--------|-------------|
| `x-dispatch-signature` | `sha256=<hmac_hex>` ā HMAC-SHA256 of the request body using your shared secret |
| `X-Dispatch-Event` | Event type: `task.new`, `task.cancel`, or `provider.verify` |
| `X-Dispatch-TaskId` | UUID of the task |
### 2. Handle `task.new` events
Request body:
```json
{
"payload_version": 1,
"event": "task.new",
"task_id": "uuid",
"description": "What needs to be done",
"category": "photo_video",
"task_type": "physical",
"location": { "address": "123 Main St", "region": "US" },
"budget": { "max_usd": 25, "currency": "USD" },
"deadline": { "complete_by": "2026-04-10T18:00:00Z", "urgency": "medium" },
"proof_required": ["photo", "gps_checkin"],
"quality_sla": "medium"
}
```
`payload_version` is the request-shape version; pin your parser to a known version and reject unknown ones. Today only `1` is sent.
Respond with:
```json
{ "accepted": true, "external_id": "your-internal-id" }
```
Or reject:
```json
{ "accepted": false, "reason": "Outside service area" }
```
### Handle `provider.verify` events
When a provider is registered, the server immediately POSTs a `provider.verify` event to confirm the endpoint is reachable and willing. **A 200 alone is not enough** ā your endpoint must return `{ "verified": true }` in the JSON body. Anything else (missing field, `false`, non-JSON) marks verification as unreachable. This makes registration require explicit consent from your endpoint, not just URL reachability.
### 3. Report completion (HTTP transport only)
POST to `http://<server>/callbacks/task/<task_id>` with headers:
- `x-provider-id`: Your provider UUID
- `x-dispatch-signature`: `sha256=<hmac_hex>` of the body
```json
{
"status": "completed",
"proof": [
{ "type": "photo", "url": "https://...", "submitted_at": "2026-04-10T12:00:00Z" }
],
"actual_cost_usd": 20,
"notes": "Task completed successfully"
}
```
### 4. Verify HMAC signatures
Always verify incoming webhooks using your shared secret:
```javascript
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
```
**HMAC canonicalization contract (load-bearing):** the signature is computed over the **exact bytes** the request was POSTed with, not over a re-serialized JSON object. When you send a callback, sign the byte string you put on the wire ā do not parse the body, re-stringify it, and sign that, because key ordering or whitespace may differ. Use `JSON.stringify(payload)` once, capture the resulting string, sign that string, send that string. The server applies the same rule on the receiving side: it captures the raw request body buffer before any JSON parser touches it.
## Smart Routing
The router automatically picks the best backend based on:
1. **Agent preferences** ā `preferred_backends` and `fallback_chain` are honored first
2. **Provider matching** ā category, task type, region, and budget compatibility
3. **Reliability** ā providers with higher completion rates are tried first
4. **Speed** ā faster providers score higher
5. **Fallback** ā the `manual` backend is always available as the ultimate fallback
## Example Agent Usage
### Python with LangGraph
```python
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
async def dispatch_photo_task():
async with MultiServerMCPClient({
"human": {
"command": "node",
"args": ["path/to/human-dispatch-mcp/dist/index.js"],
"transport": "stdio",
}
}) as client:
tools = client.get_tools()
# Register a provider first
await client.call_tool("human_register_provider", {
"name": "Photo Service Co",
"webhook_url": "https://photos.example.com/webhook",
"webhook_secret": "your-secret-that-is-at-least-32-characters",
"categories": ["photo_video"],
"task_types": ["physical"],
"regions": ["US"],
"min_budget_usd": 5,
"max_budget_usd": 100,
"max_concurrent_tasks": 20
})
# Dispatch a task
result = await client.call_tool("human_dispatch_task", {
"description": "Take a photo of the menu board at Starbucks on 5th Ave, NYC",
"category": "photo_video",
"task_type": "physical",
"location": {
"address": "5th Ave & 42nd St, New York, NY",
"region": "US"
},
"budget": {"max_usd": 15, "currency": "USD"},
"deadline": {
"complete_by": "2026-01-15T18:00:00Z",
"urgency": "medium"
},
"proof_required": ["photo", "gps_checkin"],
"quality_sla": "medium"
})
print(result)
asyncio.run(dispatch_photo_task())
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |
| `PORT` | `3000` | HTTP port (when TRANSPORT=http) |
| `MCP_AUTH_TOKEN` | ā | Bearer token required on every `POST /mcp` request when `TRANSPORT=http`. The HTTP transport refuses to start if unset. |
| `MANUAL_WEBHOOK_URL` | ā | Webhook URL for manual task notifications |
| `PROVIDERS_CONFIG` | ā | JSON array of provider objects to pre-seed on startup |
## Security
This server processes outbound HTTP requests on behalf of its callers and is intended to run inside trusted infrastructure. The relevant guarantees:
- **HTTP transport requires authentication.** `MCP_AUTH_TOKEN` is mandatory; the server refuses to start without it. Bearer comparison is constant-time (`timingSafeEqual`).
- **DNS-rebinding protection** is enabled on `POST /mcp`. The transport rejects requests whose `Host` header points at anything other than the configured loopback.
- **Outbound URL guard.** Every webhook URL the server fetches (provider registration, `MANUAL_WEBHOOK_URL`, `callback_url`, proof URLs) goes through a structured validator: HTTPS only, no loopback, no RFC1918 / link-local / unique-local hosts, with a DNS resolution check at fetch time to defeat last-second rebinds. There is no opt-out ā use a public tunnel (ngrok, cloudflared) for local testing.
- **Inbound callbacks are authenticated by HMAC, not by IP.** Each provider registers its own webhook secret. The server verifies `x-dispatch-signature` over the **raw request bytes** before parsing JSON. A per-provider token bucket limits callback flood (30 burst, 5/sec sustained).
- **Terminal-state guard.** Once a task reaches `completed`, `failed`, or `cancelled`, callbacks for that task are rejected with 409. This blocks replays, late provider retries, and provider-driven status flips.
- **Webhook payload versioning.** All outbound bodies carry `payload_version` and `event` discriminators. Pin your parser; reject unknown versions.
- **Webhook secrets never leave the server.** Provider data returned by MCP tools is sanitized to drop `webhook_secret`. The same field never appears in logs.
- **No persistence.** Tasks, providers, and per-task state live in memory. Restarting the server discards all state. If you operate this in production, terminate it cleanly so in-flight tasks fail fast rather than hang in providers.
If you discover a security issue, please open a private security advisory on GitHub rather than a public issue.
## Roadmap
- [ ] Persistent provider registry (SQLite / PostgreSQL)
- [ ] Task expiration and automatic retry
- [ ] Provider quality scoring and feedback loops
- [ ] Cost estimation before dispatch
- [ ] Batch task submission
- [ ] Provider dashboard / admin UI
- [ ] OAuth-based provider authentication
## Contributing
### Adding a New Backend Adapter
1. Create a new file in `src/services/backends/`
2. Extend `BaseBackendAdapter`
3. Implement all methods from `BackendAdapter` interface
4. Add the backend ID to the `BackendId` enum in `src/types.ts`
5. Register the adapter in `src/index.ts`
## License
MIT
TDQS
A4.4/5.0
Scored across 8 tools
Disambiguation5/5
All 8 tools have clearly distinct purposes: dispatching tasks, checking status, canceling, listing tasks, listing backends, and managing providers. No overlap or ambiguity.
Naming Consistency5/5
All tools use the 'human_' prefix followed by verb_noun (dispatch_task, get_task_status, cancel_task, etc.). Consistent and predictable pattern.
Tool Count5/5
8 tools is well-scoped for a human dispatch system, covering core operations (dispatch, status, cancellation, listing) and provider management without being excessive.
Completeness4/5
The tool surface covers the main lifecycle (dispatch, status, cancel, list) and provider CRUD. Missing a tool to modify a task in flight, but cancellation and re-dispatch suffice.
Maintenance
ActivityInactive
ResponsivenessNo issues