Skip to main content
Glama

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 REQUEST_MISMATCH.

Can't be reused

Each approval names one service (aud), lasts 60 seconds by default and works once. A second use gives REPLAYED.

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 (agent_key). Then only that app can use the approval: a copy taken from a log or a chat is refused.

Out of the AI's reach

With the secure install, the gate runs as its own locked-down user on your computer (_phygitalkeymcp on macOS, phygitalkey-mcp on Linux). The AI, even acting as you, can't change its program, read its data or add keys. Only your administrator password can.

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 (phygital-key-mcp)

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/mcp

Where

Command

macOS, Linux (script)

curl -fsSL https://github.com/Phygital-Brain/phygital-key-mcp/releases/latest/download/install.sh | sh

macOS, Linux (Homebrew)

brew install phygital-brain/tap/phygital-key-mcp

Windows (Scoop)

scoop bucket add phygital-key-mcp https://github.com/Phygital-Brain/phygital-key-mcp then scoop install phygital-key-mcp

Anywhere with Rust

cargo install --locked --git https://github.com/Phygital-Brain/phygital-key-mcp phygital-key-mcp

Anywhere with Node.js (npm)

npx -y @phygital-key/mcp stdio, or npm install -g @phygital-key/mcp

Server (Docker)

docker run -d --name phygital-key-mcp -p 127.0.0.1:7468:7468 -v phygital-key-mcp:/data ghcr.io/phygital-brain/phygital-key-mcp:latest

Secure install (macOS, Linux)

See out of the AI's reach

By hand

Download from Releases. Each file has a .sha256 next to it, and the signed lists release.txt and release2.txt give every file's SHA-256

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 demo

The 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 phygital-key-mcp stdio. No token needed: only the app that started the gate can talk to it.

Connects to a web address

Run phygital-key-mcp serve (or use the secure install, or the container), then use http://localhost:7468/mcp with the header that phygital-key-mcp token prints.

The first way (stdio), in Claude Code:

claude mcp add phygital-key-mcp -- phygital-key-mcp stdio

Claude 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

request_approval

Asks you to approve one exact request for one service, at level medium (a touch on your key) or high (fingerprint, face or PIN). It can also take the app's own key (agent_key), so only that app can use the approval.

get_approval

Waits for your answer and gets the signed approval

cancel_approval

Withdraws a request that is still waiting, or cancels a standing approval

request_standing_approval

Asks for a limited, repeated permission (up to 1000 uses, up to 30 days)

use_standing_approval

Gets a one-minute use token for one action inside those limits

record_receipt

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 high

Exit 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

APPROVAL_REQUIRED, MALFORMED

2

It was approved for this service (aud)

WRONG_AUDIENCE

3

It is in time: not before iat (30 seconds of clock difference allowed), not after exp, and it lives at most 300 seconds

EXPIRED

4

It is the exact request, byte for byte, in the fixed form

REQUEST_MISMATCH

5

It is a strong enough yes for this action (medium is a touch, high is fingerprint, face or PIN)

ASSURANCE_TOO_LOW

6

It was signed by a key in your keyring (your list of trusted keys), for the right site

UNTRUSTED_KEY

7

The passkey's signed answer (WebAuthn assertion): type, challenge, origin, site hash and ES256 signature

BAD_SIGNATURE

8

The "person was there" and "person was verified" flags match the level

ASSURANCE_TOO_LOW

9

It is the first use (used approvals are remembered)

REPLAYED

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.

NOTE

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 bash

It 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 answering

macOS

Linux

Gate user

_phygitalkeymcp

phygitalkey-mcp

Program

/Library/PhygitalKey/mcp/bin/phygital-key-mcp

/usr/lib/phygital-key-mcp/bin/phygital-key-mcp

Data

/Library/Application Support/PhygitalKey/mcp

/var/lib/phygital-key-mcp

Background services

launchd io.phygitalkey.mcp.gate, io.phygitalkey.mcp.update

systemd phygital-key-mcp.service, phygital-key-mcp-update.timer

On Linux, systemd holds the gate's port while the gate restarts for an update, so no other program can grab it in between.

IMPORTANT

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 ~/.curlrc and PATH, switching back after a failed update, and the sandbox score

Container

This repository

Builds the image, starts the gate, checks user 65532 and adding a key

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

src/main.rs

The product: its name, program, port and where releases come from, plus the MCP part (address /mcp, stdio mode)

src/mcp.rs

The six MCP tools, and the instructions the server sends to every app

tests/e2e/

124 end-to-end and attack checks: release program, Chromium with a pretend passkey, the official MCP SDK, the demo service

docs/

The MCP guides: connect your AI, tools, agents, examples, testing

examples/

Prompt snippets for your agent's instructions

packaging/container/

The container image

release-keys.pem

The public release keys that every update and installer checks against

install.sh, install-service.sh, tests/secure-install.sh, scripts/

Made from the base's templates for this product with phygital-key-mcp product-files. Never edited by hand: CI fails if they don't match. See AGENTS.md

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 tools
cancel_approvalA
Destructive

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_approvalA
Read-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

    A4.4/5.0

    Scored across 6 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count5/5

    Six tools map neatly onto the request/decision/use/receipt/cancel lifecycle without redundancy. Each tool earns its place in the workflow.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      A
      maintenance
      The 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.
      3
      550
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Verifiable action receipts for AI agents — agents sign claims locally, an independent witness countersigns and timestamps, anyone can verify offline.
      9 npm
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      A 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.
      4
      AGPL 3.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      A 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