phygital-key-mcp
OfficialThis is an MCP gate that lets an AI agent ask a person to approve exact actions with a passkey, without ever being able to approve on its own.
Request approval —
request_approvalasks a person to sign one exact request for one service (aud) atmedium(touch) orhigh(fingerprint/face/PIN) level, optionally bound to the agent's own key.Wait for the decision —
get_approvalpolls/waits (up to 120s) and returnspending,approved(with the approval string),denied,cancelledorexpired.Withdraw or revoke —
cancel_approvalcancels a waiting request or revokes an approved standing approval (returns asign_urlso services also refuse it).Standing approvals —
request_standing_approvalasks for a limited repeated permission (fixed params, bounded varying params, up to 1000 uses, up to 30 days).Use a standing approval —
use_standing_approvalgets a one-time, 60-secondapproval+use_tokenfor one request inside the approved limits.Record outcomes —
record_receiptpasses back the service's signed receipt so the person sees the action was done.No self-approval — none of the six tools can sign or approve; only the person's passkey can.
What it is
Phygital Key MCP is a Phygital Key product for AI apps that use MCP (Model Context Protocol, the common way AI apps plug in tools): Claude Code, Claude Desktop and any other MCP app. It is one program, phygital-key-mcp. It gives your AI app its tools and runs the approval page, the page where you say yes or no.
Before an AI agent deploys code, pays, deletes or sends something, it calls request_approval with the exact thing it wants to do. You read it on the approval page and approve with a passkey: your fingerprint, face, PIN or a hardware security key. Your key signs that exact request (WebAuthn, ES256 on P-256). The agent passes the signed approval to the service that does the work, and the service checks it itself, offline, before it acts.
The agent has no tool that can approve. If the request changes by even one character, the service refuses it with REQUEST_MISMATCH.
The approval page, the keys, standing approvals, receipts, the audit log, the secure install and signed updates come from the shared base, Phygital-Brain/phygital-key. This repository adds the MCP part. For agents that use A2A, see Phygital Key A2A.
Related MCP server: Witnessed
Why it exists
An AI agent with tools can deploy code, move money and send messages. Most AI apps ask "Allow?" with a button inside the app. Nobody signs that click, so the service on the other end can't tell whether a person said yes, or what they said yes to.
Phygital Key MCP gives the service proof it can check:
Promise | How |
Can't be faked | Only your key can sign (ES256 on P-256). The secret part of the key never leaves your phone, your laptop's security chip or your security key. The AI never holds a key, none of the six tools can approve, and the gate (this program) has no key that can sign approvals. |
Can't be changed | Your signature covers the exact request, written in one fixed form (RFC 8785). The service rebuilds the request it is about to run and compares the two, character by character. One changed character gives |
Can't be reused | Each approval names one service ( |
Can't be phished | Passkeys only sign for the real address of your approval page. A fake copy on another address gets nothing it can use. |
Can't be copied and used by someone else | An AI app can ask for an approval tied to its own key ( |
Out of the AI's reach | With the secure install, the gate runs as its own locked-down user on your computer ( |
Checked offline | The service only needs your public keys and a few lines of code. Nothing calls home. No account, no cloud. |
How it works
sequenceDiagram
participant A as AI app (MCP client)
participant G as Gate (phygital-key-mcp)
participant P as You + passkey
participant S as Service (verifier)
A->>G: request_approval(request, aud, level)
G-->>A: id, approve_url, status pending
G->>P: approval page shows every detail
P->>G: Approve with fingerprint, face, PIN or touch
A->>G: get_approval(id)
G-->>A: status approved, signed approval (pka.1)
A->>S: action + Phygital-Approval header
S->>S: check audience, time, exact request, level, key, signature, first use
S-->>A: done (and a signed receipt), or refused with a code
A->>G: record_receipt(id, receipt)Four roles, and what each one can and can't do:
Who | Decides | Can never |
You (the person who approves) | Yes or no, on the exact request shown | Be skipped: no key, no approval |
The agent (your AI app) | What to ask for, using the six tools | Approve, sign, or change a request after you signed it |
The gate ( | Shows the request and collects your signature | Sign an approval by itself |
The service (the one that checks, also called the verifier) | Its own name (audience), the request it runs, how strong a yes each action needs, which keys it trusts | Accept a request you did not sign |
More: how it works, rules for AI agents, the open spec.
What you see
The gate builds the approval page from the request itself, never from text the agent wrote. The page:
shows every field;
warns you about look-alike letters, invisible characters and extra spaces;
shows a short fingerprint (SHA-256) of the exact text you sign;
checks that what it shows is what will be signed, before it asks your key.
Text inside a request, like "already approved" or "SYSTEM: ...", is shown as plain text and changes nothing. How to read the page: approving. How the page protects you: approval page.
Install
npm i @phygital-key/mcpWhere | Command |
macOS, Linux (script) |
|
macOS, Linux (Homebrew) |
|
Windows (Scoop) |
|
Anywhere with Rust |
|
Anywhere with Node.js (npm) |
|
Server (Docker) |
|
Secure install (macOS, Linux) | |
By hand | Download from Releases. Each file has a |
The first release under this name was v0.4.0. Each release has files for Linux, macOS and Windows, the public container image ghcr.io/phygital-brain/phygital-key-mcp (linux/amd64, linux/arm64), the Homebrew formula and the Scoop manifest.
The npm package @phygital-key/mcp runs the same released program: it holds the build for each computer (macOS, Linux, Windows) and starts the one for yours. The release workflow publishes it with npm provenance. Update it with npm (npm install -g @phygital-key/mcp@latest).
The script downloads the latest release for your computer and checks two things before it installs anything. First, that the release's file list (release2.txt) is signed by a release key built into the script, and is still in date. Second, that the download matches its SHA-256. Then it writes one file, ~/.local/bin/phygital-key-mcp. It needs curl, tar and openssl. To update later: phygital-key-mcp update.
The container runs phygital-key-mcp --bind 0.0.0.0 serve as user 65532, with its data in /data (PHYGITAL_KEY_MCP_HOME=/data). Add your key with docker exec -it phygital-key-mcp phygital-key-mcp enroll, and get the access token with docker exec -it phygital-key-mcp phygital-key-mcp token. Only open the port on 127.0.0.1, or put an HTTPS proxy in front and pass --origin https://your.host (your own server).
Every way to install and update, and which user each one runs as: getting started, updates.
Try it in 2 minutes
phygital-key-mcp demoThe demo starts the gate on http://localhost:7468, and a pretend service on http://localhost:7469 that only acts when it gets a valid approval. It prints two things:
a one-time link (good for 10 minutes) to add your passkey: Touch ID, Windows Hello, your phone or a security key;
the line that connects Claude Code, with the access token filled in:
claude mcp add --transport http phygital-key-mcp http://localhost:7468/mcp --header "Authorization: Bearer <token>"Then ask your AI app: "Deploy api 2.4.1 to production through the demo service at http://localhost:7469." An approval page opens. Approve, and the demo service "deploys" (nothing real happens). Deny, and it refuses. Ask it to deploy 2.4.2 with the same approval, and the service refuses with REQUEST_MISMATCH.
More things to try, from payments to standing approvals: things to ask your AI.
Connect your AI app
There are two ways to connect. Both give the same six tools.
If your app | Do this |
Starts tools on your computer itself (Claude Code, Claude Desktop and most others) | Use the command |
Connects to a web address | Run |
The first way (stdio), in Claude Code:
claude mcp add phygital-key-mcp -- phygital-key-mcp stdioClaude Desktop (claude_desktop_config.json) and most other apps use this shape:
{ "mcpServers": { "phygital-key-mcp": { "command": "phygital-key-mcp", "args": ["stdio"] } } }The second way (HTTP), in Claude Code (phygital-key-mcp token prints this line with your token filled in):
claude mcp add --transport http phygital-key-mcp http://localhost:7468/mcp --header "Authorization: Bearer <token>"Add your key once, from your own terminal, with phygital-key-mcp enroll. It gives a one-time link, good for 10 minutes. In stdio mode the gate never prints that link itself, because AI apps keep that output in logs the AI could read.
The HTTP address refuses calls without the token, calls from web pages, and calls with a wrong Host header (a trick called DNS rebinding). The server calls itself phygital-key-mcp (title Phygital Key MCP) with the program's version. Every app, header, option and refusal: connect your AI app.
The six tools
The agent can ask, wait, cancel and report back. It can never approve.
Tool | What it does |
| Asks you to approve one exact request for one service, at level |
| Waits for your answer and gets the signed approval |
| Withdraws a request that is still waiting, or cancels a standing approval |
| Asks for a limited, repeated permission (up to 1000 uses, up to 30 days) |
| Gets a one-minute use token for one action inside those limits |
| Passes back the service's signed "done", so you see it on the approval page |
Each answer has a status (pending, approved, denied, cancelled, expired) and a next line that tells the agent what to do. Only approved with an approval string means yes.
When the AI app supports it, request_approval asks the app to open the approval page for you (MCP URL elicitation). Saying yes to that prompt only opens the page. It approves nothing.
For people building agents: rules for AI agents, tool reference (every input, answer and error text), prompt snippets, things to ask your AI.
Check approvals in your service
This part is for whoever runs the service. The service does the checking. It compares the approval with the request it is about to run, built from its own data. The check is the same no matter which product made the approval.
Rust, with the phygital-key-verify library from the base:
use phygital_key_verify::{Config, Keyring, Level, Verifier};
let verifier = Verifier::new(
"https://deploy.example.com", // your audience
Keyring::from_json(&std::fs::read_to_string("keyring.json")?)?, // trusted keys: `phygital-key-mcp keys`
Config { origins: vec!["https://approve.example.com".into()], rp_ids: vec!["approve.example.com".into()] },
);
let request = serde_json::json!({"type": "deploy", "params": {"service": "api", "env": "production", "version": "2.4.1"}});
verifier.verify(&approval, &request, Level::High)?; // refuses on any doubt
// ...now deploy.Any language: run the command line and look at the exit code.
phygital-key-mcp verify --approval "$APPROVAL" \
--request '{"type":"deploy","params":{"service":"api","env":"production","version":"2.4.1"}}' \
--aud https://deploy.example.com --level highExit 0 with {"ok": true, ...} means go ahead. Exit 1 with {"ok": false, "error": "REQUEST_MISMATCH"} means no. Used approvals are saved in a local file, so each one works only once. There is also a JavaScript checker with no dependencies, and it passes the same 141 shared test cases (JavaScript).
Full guide: integrate. By language: Rust, JavaScript, command line.
Safety, verification and authority
What the service checks, in order
When in doubt, the checker says no. The first check that fails refuses the approval, with a code that says why.
Step | What is checked | Refused with |
1 | The approval is there, is clean JSON, has only known fields, and isn't too big or too deep |
|
2 | It was approved for this service ( |
|
3 | It is in time: not before |
|
4 | It is the exact request, byte for byte, in the fixed form |
|
5 | It is a strong enough yes for this action ( |
|
6 | It was signed by a key in your keyring (your list of trusted keys), for the right site |
|
7 | The passkey's signed answer (WebAuthn assertion): type, challenge, origin, site hash and ES256 signature |
|
8 | The "person was there" and "person was verified" flags match the level |
|
9 | It is the first use (used approvals are remembered) |
|
Every code, why it happens and what to do: error codes. What an agent should do with each one: when the service refuses.
The cryptography
Your key signs a challenge that locks in every detail of the approval:
$$\text{challenge} = \mathrm{SHA256}\big(\texttt{"phygital-key/approval/v1"} ,|, \texttt{0x00} ,|, \mathrm{JCS}(\text{claims})\big)$$
The signature is ES256 (ECDSA on P-256 with SHA-256), made by your passkey (a WebAuthn assertion).
Hashes are SHA-256.
The fixed form is RFC 8785 (JCS): strict JSON, only safe whole numbers, no decimals, and text in Unicode NFC. Other text is refused, never rewritten.
Receipts and use tokens are ES256 JWS. The audit log is a SHA-256 hash chain, so a changed entry shows. Release lists are signed with ECDSA P-256, and also with ML-DSA-65, a signature built to resist future quantum computers.
Deep dives: canonical form, WebAuthn checks, key history.
Approvals aresigned, not encrypted. The gate, the service and anyone holding an approval can read the request. Keep passwords and card numbers out of requests. Use HTTPS for any gate that isn't on localhost. Why: decision 33.
Every key you add or remove is itself an approval, signed by a key you already trust. So a service can check your whole key history starting from your first key's fingerprint (phygital-key-mcp keys --check <fingerprint>).
Out of the AI's reach
An AI agent on your computer runs as you, so it could change files you own, including a gate you installed yourself. The secure install puts the gate where the AI can't reach it (macOS and Linux). Open a new terminal and run:
curl -q -fsSL https://raw.githubusercontent.com/Phygital-Brain/phygital-key-mcp/v0.6.3/install-service.sh | sudo bashIt downloads the official release itself and checks it against the signed release list. It installs it into folders only the administrator can change, and runs it as its own user. If any other program is already using port 7468, it refuses to run. Then:
phygital-key-mcp-admin enroll # add your key
phygital-key-mcp-admin token # the line to paste into your AI app
phygital-key-mcp-admin status # check that the real gate is the one answeringmacOS | Linux | |
Gate user |
|
|
Program |
|
|
Data |
|
|
Background services | launchd | systemd |
On Linux, systemd holds the gate's port while the gate restarts for an update, so no other program can grab it in between.
The secure install only protects the gate if the AI can't get your administrator password. Run it from a terminal you trust, don't let your user runsudo without a password, and never type that password where the AI can see it. Each admin command asks for it.
Details, every folder and every refusal: secure install.
Signed automatic updates
The secure install checks for updates once a day. It installs a new release only if all of this is true:
its file list (
release2.txt) is signed by the release keys built into this program and its installers (ECDSA P-256, plus ML-DSA-65 once that key exists);the list is still in date, and not older than one this computer already saw;
it is newer than the version you have.
The signing key is a secret kept in this repository's GitHub environment release, and only a release run started from a v* tag can use it.
The update waits while a standing approval is active (7 days at most), because a restart would end it. If the new gate doesn't start and pass its checks, the previous version comes back. The update runs as the administrator with a clean setup, so your own settings like ~/.curlrc and PATH are never used.
phygital-key-mcp-admin status shows the last result, phygital-key-mcp-admin update updates now, and --no-auto-update turns automatic updates off. How updates are protected: the update channel. Every way to update: updates.
How it is tested
What | Where | How |
End to end, and attacks | This repository | 124 checks with the real release program, a real Chromium browser with a pretend passkey, the official MCP TypeScript SDK and the demo service. They include attacks: changed requests, prompt injection, forged signatures, reused approvals, DNS rebinding, key rollback |
Secure install | This repository | 36 checks on Linux, 32 on macOS. It is installed for real in CI, then attacked as the user. Also checked: the update job (started by systemd and launchd), refusing a badly signed release, a planted |
Container | This repository | Builds the image, starts the gate, checks user |
Generated files | This repository | The files made from the base's templates must match the ones saved here |
Shared test cases | Base | 141 cases that both the Rust and the JavaScript checkers must pass, plus Google's Wycheproof cases, a cross-check with SimpleWebAuthn, property tests, fuzzing and mutation testing |
Security reviews | Base | 9 separate AI security reviews. Every problem found was fixed or written down as a known limit |
CI runs on every change on Linux, macOS and Windows, and again after each daily base update. Job by job: testing. The engine's tests: testing in the base. Found a problem? Report it privately.
Standing approvals
For small actions that repeat, you can say yes once, with limits:
Allow payment to ACME Ltd in EUR, up to 20.00 each, at most 5 times, for 7 days.
The agent asks with request_standing_approval (up to 1000 uses, up to 30 days). For each action it calls use_standing_approval and gets a use token that lasts one minute. It sends that token with the standing approval (the Phygital-Approval and Phygital-Use headers). The service checks both and counts the uses itself. A different payee, a bigger amount or an extra field is refused with NOT_ALLOWED. Standing approvals always need your fingerprint, face or PIN.
You can cancel it on the approval page, or ask the agent to call cancel_approval. New uses stop at once. One more touch signs a cancellation notice that services read from /.well-known/phygital-key-revoked.json. Guides: for people, for services, cancellations.
Receipts
After it acts, a service can send back a signed receipt ("done" or "failed"). The agent passes it on with record_receipt. Your approval page shows it only if it was signed by the key you saved for that service:
phygital-key-mcp trust-service --aud https://deploy.example.com --key <its receipt_public_key>Format and checks: receipts.
All the guides
The shared guides are in the base repository: all the guides (approving, keys, standing approvals, secure install, checking approvals in your service, security). Guides for one product (connecting AI apps, its tools, prompts) are in that product's repository.
This product's guides: Connect your AI app · MCP tools · Agents guide · Things to ask · Prompt snippets · Testing
This repository
Path | What |
The product: its name, program, port and where releases come from, plus the MCP part (address | |
The six MCP tools, and the instructions the server sends to every app | |
124 end-to-end and attack checks: release program, Chromium with a pretend passkey, the official MCP SDK, the demo service | |
The MCP guides: connect your AI, tools, agents, examples, testing | |
Prompt snippets for your agent's instructions | |
The container image | |
The public release keys that every update and installer checks against | |
| Made from the base's templates for this product with |
Everything else comes from the base, Phygital-Brain/phygital-key: the approval page, keys, standing approvals, receipts, audit log, secure install, signed updates and the checker. Every day the sync-base workflow moves this product to the base's latest version. It keeps the change only if the tests and the 124 end-to-end checks pass, then saves it and runs the full CI. How the base and the products fit together: architecture.
Changes: CHANGELOG.md. Security problems: SECURITY.md. Contributing: CONTRIBUTING.md.
Privacy
The gate keeps its data in one folder: ~/.phygital-key-mcp, or after the secure install, a folder only the gate's own user can read. In it:
the public keyring (your public keys);
an audit log, chained so changes show (ids, hashes and results, never the request itself);
saved service keys;
signed cancellations;
the access token.
Requests that are still waiting live only in memory. No analytics, no accounts, no tracking. The fonts are built into the program, so the approval page loads nothing from the internet. More: privacy.
Open source and credit
Phygital Key MCP is free and open source under the Apache-2.0 license. You can use it at home or at work, change it, put it in a product and sell that product. You can read every line of code that handles your approvals, and check the release you run against its signed SHA-256 list.
Keep the NOTICE file, with the line "Created by Phygital-Key", in every copy or changed version (Apache-2.0, section 4(d)).
Contributions are welcome under the same license: see CONTRIBUTING.md. Shared behaviour is changed in the base, never here.
No warranty
Provided "as is", without warranty of any kind, as the license says. It hasn't been audited yet: try it with the demo first, read the threat model, and keep a second key added as a backup.
License
Apache-2.0. Created by Phygital-Key.
Available Tools
6 toolscancel_approvalADestructive
Withdraw a request that is still waiting, or revoke an approved standing approval. Cannot be undone. A revoke stops new uses at once and returns sign_url: show it to the person, so services refuse the standing approval too.
Inputs: id (required).
get_approvalARead-onlyIdempotent
Wait for the person's decision on a request. Returns status: pending (call again), approved (with the approval string to send with the action), denied, cancelled or expired (then do not act). Changes nothing; safe to call again.
Inputs: id (required), wait_seconds.
record_receiptA
After the service acted, pass back the signed receipt it returned, so the person sees it was done. Only records it: one per approval, or per use of a standing approval. If no receipt key is pinned for that service, tell the person and don't retry.
Inputs: id (required), receipt (required).
request_approvalA
Ask a person to approve one exact action with their passkey, fingerprint or hardware key. Returns an id and approve_url, not the decision: then call get_approval. For many similar actions, use request_standing_approval. Nothing is approved until the person signs.
Inputs: aud (required), level (required), request (required), agent_key.
request_standing_approvalA
Ask a person once to allow the same kind of action within fixed limits, up to max_uses times, for up to days days. Then call get_approval, and use_standing_approval for each action. For one action, use request_approval. Use sparingly: the person sees every limit.
Inputs: aud (required), days (required), vary (required), fixed (required), +2 more.
use_standing_approvalA
Use an approved standing approval for one exact request inside its limits. Counts one use and returns approval and use_token: send both with that request (headers Phygital-Approval and Phygital-Use). The token works once, within 60 seconds.
Inputs: id (required), request (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
The six tools each target a distinct step of the approval lifecycle, and descriptions explicitly cross-reference when to prefer one over another (e.g. request_approval vs request_standing_approval vs use_standing_approval). The only mild overlap is among the three standing-approval-related tools, but the guidance ('For one action, use request_approval') keeps them separable.
All tools follow a clean verb_noun snake_case pattern: cancel_approval, get_approval, record_receipt, request_approval, request_standing_approval, use_standing_approval. The pattern is fully predictable and readable.
Six tools map neatly onto the request/decision/use/receipt/cancel lifecycle without redundancy. Each tool earns its place in the workflow.
The surface covers the full lifecycle: request (single and standing), poll decision, use a standing approval, record a receipt, and cancel/revoke. There is no listing/introspection tool (e.g. list outstanding approvals), a minor gap agents can work around by tracking ids.
Maintenance
Related MCP Connectors
Human-in-the-loop approval for agent actions, with verifiable action-bound receipts.
Human approval for irreversible AI agent actions, bound to the exact tool call by a passkey tap
Human approval for AI agents: one ask, one person, one boolean.
Governed AI actions with signed, verifiable receipts: free keyless reads, human-approved writes.
Related MCP Servers
AlicenseAqualityAmaintenanceThe accountability layer for AI agents — a named human's signed yes before an agent does anything irreversible (payment, record change, deploy), then an offline-verifiable Trust Receipt. Apache-2.0, formally verified.3550Apache 2.0- AlicenseNot gradedqualityDmaintenanceVerifiable action receipts for AI agents — agents sign claims locally, an independent witness countersigns and timestamps, anyone can verify offline.9 npmMIT

infraveil-guardofficial
AlicenseAqualityCmaintenanceA cooperative guardrail for AI agents that blocks destructive shell commands, SQL statements, or cloud operations until a human approves them out-of-band, with a tamper-evident local ledger.4AGPL 3.0- AlicenseNot gradedqualityDmaintenanceA human-in-the-loop governance interlock for AI agents. Agents propose changes, a human countersigns the exact plan, and then it executes stage by stage with precondition checks, verification, and auditing.Apache 2.0