{
  "markdown": "# human-dispatch-mcp\n\n🌐 **[humandispatch.ai](https://humandispatch.ai)** — Homepage & provider docs\n\n**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.\n\nRoutes 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.\n\n## Quick Start\n\n```bash\n# Clone and install\ngit clone https://github.com/zyntarasystems/human-dispatch-mcp.git\ncd human-dispatch-mcp\nnpm install\n\n# Configure (optional — works out of the box with manual fallback)\ncp .env.example .env\n\n# Build and run\nnpm run build\nnode dist/index.js\n```\n\n## Testing with MCP Inspector\n\nThe easiest way to verify the server is working:\n\n```bash\nnpx @modelcontextprotocol/inspector node dist/index.js\n```\n\nOpen `http://localhost:5173`, enter the proxy session token shown in your terminal, and click **Connect**.\n\n### Test sequence:\n\n1. **List backends** — call `human_list_backends` to see `webhook_provider` and `manual`\n\n2. **Register a provider** — call `human_register_provider`:\n```json\n{\n  \"name\": \"Test Provider\",\n  \"webhook_url\": \"https://webhook.site/your-uuid\",\n  \"webhook_secret\": \"a-secret-that-is-at-least-32-chars-long!\",\n  \"categories\": [\"digital_micro\"],\n  \"task_types\": [\"digital\"],\n  \"regions\": [\"*\"],\n  \"min_budget_usd\": 0,\n  \"max_budget_usd\": 500,\n  \"max_concurrent_tasks\": 10\n}\n```\n\n3. **Dispatch a task** — call `human_dispatch_task` with **Raw JSON** input mode:\n```json\n{\n  \"description\": \"Test task — verify the MCP server is routing correctly\",\n  \"category\": \"digital_micro\",\n  \"task_type\": \"digital\",\n  \"budget\": { \"max_usd\": 5, \"currency\": \"USD\" },\n  \"deadline\": {\n    \"complete_by\": \"2026-04-10T18:00:00Z\",\n    \"urgency\": \"low\"\n  },\n  \"proof_required\": [\"text_report\"],\n  \"quality_sla\": \"low\",\n  \"callback_url\": null\n}\n```\n\nThe task should route to your registered provider. If no providers match, it falls through to the manual backend.\n\n## MCP Client Configuration\n\n### Claude Desktop / Cursor / Any MCP Client\n\n```json\n{\n  \"mcpServers\": {\n    \"human-dispatch\": {\n      \"command\": \"npx\",\n      \"args\": [\"human-dispatch-mcp\"]\n    }\n  }\n}\n```\n\n### HTTP Transport\n\n> **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.\n\n> **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.\n\n```json\n{\n  \"mcpServers\": {\n    \"human-dispatch\": {\n      \"command\": \"npx\",\n      \"args\": [\"human-dispatch-mcp\"],\n      \"env\": {\n        \"TRANSPORT\": \"http\",\n        \"PORT\": \"3000\",\n        \"MCP_AUTH_TOKEN\": \"a-long-random-string-32-chars-or-more\"\n      }\n    }\n  }\n}\n```\n\n## Tools Reference\n\n| Tool | Description |\n|------|-------------|\n| `human_dispatch_task` | Submit a task to be completed by a human worker via the best matching provider |\n| `human_get_task_status` | Poll the current status, worker info, and proof submissions for a task |\n| `human_cancel_task` | Cancel a pending or in-progress task |\n| `human_list_tasks` | List tasks with filters (status, backend, category) and pagination |\n| `human_list_backends` | Show available backends, their configuration status, and capabilities |\n| `human_register_provider` | Register a webhook provider to receive dispatched tasks |\n| `human_list_providers` | List registered providers with stats and filters |\n| `human_remove_provider` | Deregister a webhook provider |\n\n## Architecture\n\n```\n┌─────────────┐\n│   AI Agent   │\n│ (Claude, etc)│\n└──────┬───────┘\n       │ MCP Protocol (stdio or HTTP)\n       ▼\n┌──────────────────────────────────────┐\n│     human-dispatch-mcp Server        │\n│                                      │\n│  ┌────────────┐  ┌────────────────┐  │\n│  │ Task Store │  │ Provider       │  │\n│  │ (in-memory)│  │ Registry       │  │\n│  └────────────┘  └───────┬────────┘  │\n│                          │           │\n│  ┌────────────┐  ┌───────▼────────┐  │\n│  │   Router   │──│  Webhook       │  │\n│  │  (scoring) │  │  Provider      │  │\n│  └──────┬─────┘  │  Adapter       │  │\n│         │        └───────┬────────┘  │\n│         │                │           │\n│         │    ┌───────────▼─────────┐ │\n│         │    │ Provider A (law)    │ │\n│         │    │ Provider B (VA)     │ │\n│         │    │ Provider C (photos) │ │\n│         │    └─────────────────────┘ │\n│         ▼                            │\n│  ┌────────────┐                      │\n│  │   Manual   │ (always-on fallback) │\n│  │  Adapter   │                      │\n│  └────────────┘                      │\n└──────────────────────────────────────┘\n```\n\n## For Service Providers\n\nAny business can register as a provider to receive AI-dispatched tasks. Here's how:\n\n### 1. Set up a webhook endpoint\n\nYour endpoint receives POST requests with these headers:\n\n| Header | Description |\n|--------|-------------|\n| `x-dispatch-signature` | `sha256=<hmac_hex>` — HMAC-SHA256 of the request body using your shared secret |\n| `X-Dispatch-Event` | Event type: `task.new`, `task.cancel`, or `provider.verify` |\n| `X-Dispatch-TaskId` | UUID of the task |\n\n### 2. Handle `task.new` events\n\nRequest body:\n```json\n{\n  \"payload_version\": 1,\n  \"event\": \"task.new\",\n  \"task_id\": \"uuid\",\n  \"description\": \"What needs to be done\",\n  \"category\": \"photo_video\",\n  \"task_type\": \"physical\",\n  \"location\": { \"address\": \"123 Main St\", \"region\": \"US\" },\n  \"budget\": { \"max_usd\": 25, \"currency\": \"USD\" },\n  \"deadline\": { \"complete_by\": \"2026-04-10T18:00:00Z\", \"urgency\": \"medium\" },\n  \"proof_required\": [\"photo\", \"gps_checkin\"],\n  \"quality_sla\": \"medium\"\n}\n```\n\n`payload_version` is the request-shape version; pin your parser to a known version and reject unknown ones. Today only `1` is sent.\n\nRespond with:\n```json\n{ \"accepted\": true, \"external_id\": \"your-internal-id\" }\n```\n\nOr reject:\n```json\n{ \"accepted\": false, \"reason\": \"Outside service area\" }\n```\n\n### Handle `provider.verify` events\n\nWhen 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.\n\n### 3. Report completion (HTTP transport only)\n\nPOST to `http://<server>/callbacks/task/<task_id>` with headers:\n- `x-provider-id`: Your provider UUID\n- `x-dispatch-signature`: `sha256=<hmac_hex>` of the body\n\n```json\n{\n  \"status\": \"completed\",\n  \"proof\": [\n    { \"type\": \"photo\", \"url\": \"https://...\", \"submitted_at\": \"2026-04-10T12:00:00Z\" }\n  ],\n  \"actual_cost_usd\": 20,\n  \"notes\": \"Task completed successfully\"\n}\n```\n\n### 4. Verify HMAC signatures\n\nAlways verify incoming webhooks using your shared secret:\n\n```javascript\nconst crypto = require('crypto');\nconst expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');\nconst valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));\n```\n\n**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.\n\n## Smart Routing\n\nThe router automatically picks the best backend based on:\n1. **Agent preferences** — `preferred_backends` and `fallback_chain` are honored first\n2. **Provider matching** — category, task type, region, and budget compatibility\n3. **Reliability** — providers with higher completion rates are tried first\n4. **Speed** — faster providers score higher\n5. **Fallback** — the `manual` backend is always available as the ultimate fallback\n\n## Example Agent Usage\n\n### Python with LangGraph\n\n```python\nimport asyncio\nfrom langchain_mcp_adapters.client import MultiServerMCPClient\n\nasync def dispatch_photo_task():\n    async with MultiServerMCPClient({\n        \"human\": {\n            \"command\": \"node\",\n            \"args\": [\"path/to/human-dispatch-mcp/dist/index.js\"],\n            \"transport\": \"stdio\",\n        }\n    }) as client:\n        tools = client.get_tools()\n\n        # Register a provider first\n        await client.call_tool(\"human_register_provider\", {\n            \"name\": \"Photo Service Co\",\n            \"webhook_url\": \"https://photos.example.com/webhook\",\n            \"webhook_secret\": \"your-secret-that-is-at-least-32-characters\",\n            \"categories\": [\"photo_video\"],\n            \"task_types\": [\"physical\"],\n            \"regions\": [\"US\"],\n            \"min_budget_usd\": 5,\n            \"max_budget_usd\": 100,\n            \"max_concurrent_tasks\": 20\n        })\n\n        # Dispatch a task\n        result = await client.call_tool(\"human_dispatch_task\", {\n            \"description\": \"Take a photo of the menu board at Starbucks on 5th Ave, NYC\",\n            \"category\": \"photo_video\",\n            \"task_type\": \"physical\",\n            \"location\": {\n                \"address\": \"5th Ave & 42nd St, New York, NY\",\n                \"region\": \"US\"\n            },\n            \"budget\": {\"max_usd\": 15, \"currency\": \"USD\"},\n            \"deadline\": {\n                \"complete_by\": \"2026-01-15T18:00:00Z\",\n                \"urgency\": \"medium\"\n            },\n            \"proof_required\": [\"photo\", \"gps_checkin\"],\n            \"quality_sla\": \"medium\"\n        })\n        print(result)\n\nasyncio.run(dispatch_photo_task())\n```\n\n## Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `TRANSPORT` | `stdio` | Transport mode: `stdio` or `http` |\n| `PORT` | `3000` | HTTP port (when TRANSPORT=http) |\n| `MCP_AUTH_TOKEN` | — | Bearer token required on every `POST /mcp` request when `TRANSPORT=http`. The HTTP transport refuses to start if unset. |\n| `MANUAL_WEBHOOK_URL` | — | Webhook URL for manual task notifications |\n| `PROVIDERS_CONFIG` | — | JSON array of provider objects to pre-seed on startup |\n\n## Security\n\nThis server processes outbound HTTP requests on behalf of its callers and is intended to run inside trusted infrastructure. The relevant guarantees:\n\n- **HTTP transport requires authentication.** `MCP_AUTH_TOKEN` is mandatory; the server refuses to start without it. Bearer comparison is constant-time (`timingSafeEqual`).\n- **DNS-rebinding protection** is enabled on `POST /mcp`. The transport rejects requests whose `Host` header points at anything other than the configured loopback.\n- **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.\n- **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).\n- **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.\n- **Webhook payload versioning.** All outbound bodies carry `payload_version` and `event` discriminators. Pin your parser; reject unknown versions.\n- **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.\n- **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.\n\nIf you discover a security issue, please open a private security advisory on GitHub rather than a public issue.\n\n## Roadmap\n\n- [ ] Persistent provider registry (SQLite / PostgreSQL)\n- [ ] Task expiration and automatic retry\n- [ ] Provider quality scoring and feedback loops\n- [ ] Cost estimation before dispatch\n- [ ] Batch task submission\n- [ ] Provider dashboard / admin UI\n- [ ] OAuth-based provider authentication\n\n## Contributing\n\n### Adding a New Backend Adapter\n\n1. Create a new file in `src/services/backends/`\n2. Extend `BaseBackendAdapter`\n3. Implement all methods from `BackendAdapter` interface\n4. Add the backend ID to the `BackendId` enum in `src/types.ts`\n5. Register the adapter in `src/index.ts`\n\n## License\n\nMIT\n",
  "bytes": 13406,
  "sha": "ea84d11186ed5fbf0f5a6016602fa2c5c58ce7d3b8f0f2bba6e6e0ec8f0aef94",
  "repo_slug": "zyntarasystems/human-dispatch-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_zyntarasystems_human_dispatch__b586b4e1/readme"
}