Human-in-the-loop authentication for AI agents
Biometric approval with PKI-signed attestation — via Model Context Protocol (MCP).
When an agent hits a sensitive step, this MCP server pauses the workflow, sends an iVALT push to the approver's phone, and returns a PKI-signed attestation after biometric approval (FaceID / fingerprint, plus optional location, time, and device checks).
Agent step → request_approval → iVALT push → human approves → PKI attestation → agent continues
This one repo is the single source of truth for every distribution surface. Pick the path that matches what you're doing:
| Use case 1 — Install the plugin | Use case 2 — Use the MCP in your agent workflow | |
|---|---|---|
| Who | A developer working inside Cursor or Claude | An agent / app calling iVALT programmatically |
| What ships | MCP server + iv-auth skill, one click |
MCP server only |
| How | Cursor plugin / deeplink · Claude .mcpb / Claude Code plugin |
npx @ivalt/agent-auth over stdio, or hosted HTTP |
| Guide | install/cursor.md · install/claude.md |
see below |
The skill matters: the MCP server gives the capability, the iv-auth skill gives the agent the instructions to use it correctly (async flow, gate on approved). Use case 1 ships both together; use case 2 is the raw server.
| Host | Method | Link |
|---|---|---|
| Cursor | One-click deeplink, mcp.json, or plugin (MCP + skill) |
install/cursor.md |
| Claude Desktop | .mcpb Desktop Extension (one-click) |
install/claude.md |
| Claude Code | /plugin marketplace add iVALT-Inc/agent-auth (MCP + skill) |
install/claude.md |
The packaging lives alongside the server: .cursor-plugin/plugin.json (Cursor), .claude-plugin/ (Claude Code), manifest.json (Claude Desktop .mcpb).
No clone or build — run directly from npm:
npx -y @ivalt/agent-auth{
"mcpServers": {
"ivalt": {
"command": "npx",
"args": ["-y", "@ivalt/agent-auth"],
"env": {
"IVALT_API_KEY": "your-key",
"IVALT_DEFAULT_MOBILE": "+1..."
}
}
}
}from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"ivalt": {
"command": "npx",
"args": ["-y", "@ivalt/agent-auth"],
"env": {"IVALT_API_KEY": "your-key", "IVALT_DEFAULT_MOBILE": "+1..."},
"transport": "stdio",
}
})
tools = await client.get_tools() # request_approval, request_approval_async, check_status- Node.js 18+
- iVALT credential (optional) — without
IVALT_API_KEY, the server uses iVALT's public demo proxy (ondemandid.com); set a key to callapi.ivalt.comdirectly - Approver phone — set
IVALT_DEFAULT_MOBILE(E.164) or passapprover_mobileon each tool call
- stdio — launched locally via
npx(Cursor, Claude Desktop, LangGraph) - HTTP (Streamable-HTTP) — hostable endpoint for multi-tenant / remote use (Docker)
The agent never sees the iVALT key — it lives only in this server's env.
| Tool | Behavior |
|---|---|
request_approval |
Blocking. Sends the request and waits up to timeout_s (default 90, max 120) for approval. Returns the attestation. |
request_approval_async |
Returns a request_id immediately. For long-running cloud agents. |
check_status |
Polls a request_id → pending / approved (with attestation) / denied / expired. |
Shared inputs: action (required), reason, approver_mobile (E.164, overrides the default — this is how one endpoint serves many tenants), factors (biometric | location | time | device).
Approved responses return a normalized attestation:
{
"status": "approved",
"attestation": {
"approved": true,
"request_id": "a518d10b-...",
"approver": { "mobile": "+1...", "name": "Jane Doe", "email": "jane@example.com" },
"approver_mobile": "+1...",
"request_from": "iVALT MCP: <action>",
"factors_verified": { "biometric": true, "location": true, "time": false, "device": true },
"location": { "latitude": "32.84...", "longitude": "-79.86...", "address": "..." },
"signed_at": "2026-06-26T...Z",
"pki": { "public_key": "MIICIjANBgkq...", "signature": null, "key_ref": "a518d10b-..." },
"raw": { }
}
}Notes on the live shape: iVALT returns the device's registered public key in raw.imei (mapped to pki.public_key), the iVALT request UUID in raw.request_id, and geo data when the device shares it. This endpoint does not return a separate detached signature, so pki.signature is typically null while pki.public_key carries the PKI material. The complete upstream payload is always preserved under raw.
All config is via environment variables (see .env.example).
| Var | Default | Purpose |
|---|---|---|
IVALT_API_KEY / IVALT_CONNECTION_ID |
(none) | iVALT credential; presence switches upstream to direct. |
IVALT_UPSTREAM |
auto | Force direct or proxy. |
IVALT_DEFAULT_MOBILE |
(none) | Default approver phone (E.164). If unset, callers must pass approver_mobile per request. |
IVALT_REQUEST_FROM |
iVALT MCP |
Label on the approval request. |
IVALT_DEFAULT_FACTORS |
(none) | Comma list of default factors. |
PORT |
8080 |
HTTP transport port. |
MCP_AUTH_TOKEN |
(none) | Bearer token required on POST /mcp. |
To connect to a hosted server instead of running locally (no Node needed on the client):
client = MultiServerMCPClient({
"ivalt": {
"url": "https://your-host/mcp",
"transport": "streamable_http",
"headers": {"Authorization": "Bearer <MCP_AUTH_TOKEN>"},
}
})One source, one version, many shelves. The version in package.json is the single source of truth; npm run sync:version stamps it into every manifest so they never drift.
.
├── package.json # npm: @ivalt/agent-auth (the MCP server)
├── skills/iv-auth/SKILL.md # the skill, shipped with the plugins (and npm)
├── manifest.json # Claude Desktop .mcpb (built into dist/)
├── .cursor-plugin/plugin.json # Cursor plugin (MCP + skill)
├── .claude-plugin/ # Claude Code plugin + marketplace (MCP + skill)
├── install/ # one-click install docs (deeplink, .mcpb, /plugin)
└── .github/workflows/release.yml # tag vX.Y.Z → npm publish + attach .mcpb
Cut a release:
npm version patch # bump package.json (the source of truth)
npm run sync:version # propagate to all manifests, commit
git push --follow-tags # the tag triggers .github/workflows/release.ymlThe workflow publishes to npm and attaches dist/ivalt-agent-auth.mcpb to the GitHub Release. Registry listings (official MCP registry, Smithery, Glama, etc.) and the Cursor/Claude marketplaces point back at this repo + npm. Requires an NPM_TOKEN repo secret.
Build the .mcpb locally:
npm run build:mcpb # → dist/ivalt-agent-auth.mcpbgit clone https://github.com/iVALT-Inc/agent-auth.git
cd agent-auth
npm install
# stdio (what Cursor/Claude/LangGraph spawn)
npm run start:stdio
# HTTP transport
MCP_AUTH_TOKEN=secret PORT=8080 npm run start:http
curl http://localhost:8080/healthQuick wiring check:
node test/smoke.js stdio
node test/smoke.js http http://localhost:8080/mcp secret
# add SMOKE_FIRE=1 to also fire a real push to IVALT_DEFAULT_MOBILEdocker build -t ivalt-mcp .
docker run -p 8080:8080 \
-e MCP_AUTH_TOKEN=secret \
-e IVALT_API_KEY=your-key \
-e IVALT_DEFAULT_MOBILE=+1... \
ivalt-mcp
curl http://localhost:8080/healthThe multi-tenant router connects to POST /mcp with the bearer token and passes a per-tenant approver_mobile in each tool call.
request_idencodes the approver mobile + issue time; iVALT's result endpoint keys on the mobile, so avoid concurrent in-flight requests for the same phone.- Approval window is ~60s; blocking
request_approvalpolls every 2s. - HTTP runs in stateless mode (
GET/DELETE /mcpreturn 405).