Written for the agent, not the person operating it — read before your first tool call.
Agent skill: read this before you connect
You are an agent about to add Agent Passkey as an MCP server, or you have just been told to sign someone in with it. This page is written for you, not the person operating you — read it before your first tool call.
What this is
Agent Passkey holds a person's passkey private keys and signs WebAuthn assertions with them on request. You drive a browser to a relying party and ask this service to sign the bytes that party wants signed. You never receive a private key, a password, a cookie or a recovery code — no argument to any tool here can carry one, and no result can return one.
What you must bring
A browser you can navigate and click in. If it has the Agent Passkey extension (the person's own Chrome usually does — Claude in Chrome drives exactly that browser), navigating and clicking is all you need: no JavaScript tool. Without the extension you also need to run a script in the page. The remote server (https://app.solospace.dev/mcp) has no browser — it signs bytes, you drive the page. If your only tools are this connector's and you have no browser access, say so plainly rather than guessing at a sign-in. A local stdio server (agent-passkey-mcp) drives its own Chromium and does the whole thing in one call — check your tool list for passkey_signin to know which situation you're in.
The shape of the procedure
passkey_list— which sites you may sign in to, and the operating notes for each.- Call
passkey_begin_signin(withpageUrlif you are already looking at the passkey prompt) and do what itsnextsays. Normally: navigate toarmUrl. The extension binds the ticket to your tab and sends you back. If the arm page says there is no extension, followifNotArmed— evaluateshimwhere the site allows it, or ask the person the one sentence it gives you. - Drive the page to the passkey prompt. Most sites hide it behind "Try another way" until an email or button tells them who is signing in — this is the step that actually fails, and the connector's notes exist to prevent it.
- Click the site's own passkey button ("Use passkey", "Sign in with a passkey"), then watch the page or
passkey_signin_status({ ticket }). The extension (or the page) talks to the service itself. You are not expected to carry cryptographic material by hand — a fallback path exists for a page that can't reach the service, but it is the fallback, not the normal case. - The site sets its session. Done.
Call passkey_howto once, right after you connect and before your first sign-in. It is a tool, not only a static page, because it is the one affordance guaranteed to reach you at handshake time — and it returns this same procedure with your own connectors, credential IDs and the exact page script filled in.
Which account: names and aliases
A site often shows the account under a different name from the one the connector was enrolled with — GitHub's "Confirm access" says *Signed in as @TheDevDapper* while the connector says elishavas2@gmail.com. passkey_list returns aliases for each connector: other names the site itself gave for the account at enrolment, or that the owner added on the dashboard — never names an agent supplied. Declare the name you see with expectAccount; if it equals the account, its label or any alias (exactly, case-insensitive, a leading @ ignored), it is the same account. If it matches none, the refusal lists every account with all its names: compare, and if nothing matches, ask — do not guess that two names are one account.
Strict-CSP sites: GitHub sudo mode
GitHub's sensitive pages (/settings/*, /apps/*/installations, deletes, transfers) stop at Confirm access with a Use passkey button. GitHub's CSP blocks any page script from reaching this service, so the shim cannot finish there and a script-injection fallback is not the answer. Do this instead: passkey_begin_signin({ credentialId, expectAccount: "<the @login you see>", pageUrl: "<the page you are on>" }) → navigate to armUrl → click Use passkey. No javascript tool is involved, nothing base64 crosses you, and the signature is audited exactly like any other ticket signature.
How to read a refusal
Every result this service returns carries state, retryable, terminal and next. Two of the four read backwards from instinct:
needs_authorization(HTTP 403,insufficient_scope) is not a stop — the person never granted this permission. Authorize again, asking for the scope named innext.deniedlooks like an ordinary tool error (isError: true) and is not one — a human, or policy, or an origin check said no on the merits.terminal: true,retryable: false. Stop and report it; do not vary the arguments to try again.
pending is a *success* mid-flight: a human is being asked on their phone, right now. You have no sleep primitive, so send waitUpToMs and let the server hold the call open rather than polling in a loop.
Full table: Failure modes.
Reading back what happened
You do not have to guess whether a signature was made or an approval answered:
passkey_approval_status({ approvalId, waitUpToMs })— one approval, held open server-side until a human decides or the wait runs out. Use it instead of re-sending the sign-in call, which would notify the person again.passkey_audit({ limit, before })— your own audit rows (needsaudit:read): every assert, its decision and the reason phrase. Nothing another agent did is visible here.passkey_run_status({ runId })/passkey_automation_runs({ automationId })— the runs of an automation, when you are the runner. Begin withpasskey_run_begin, end withpasskey_run_end, and every signature in between is stamped with the run's request id.passkey_check_access— what this connection may do right now, tool by tool, before you plan.
If you are a script, not an agent
The same tools are reachable as plain HTTP under https://app.solospace.dev/v1/… with an API key the person mints on their dashboard (Authorization: Bearer apk_key_…). A key is a grant exactly like yours — same accounts, scopes, expiry, audit line and revoke button — so a cron job needs no OAuth dance and no MCP client. See API keys.
Honest limits
- Enrolling a passkey needs the
enrolscope, and even then only *after* a human has signed in to the site in the browser you drive — the service holds no password and cannot do that step. Without the scope, a human does it with the extension. There is no fallback credential. - Roughly half of "passkey support" is second-factor only, after a password — this service cannot get into those accounts at all.
silentmode signsUP=0/UV=0, truthfully; most sites reject it. That is not a bug to work around.
Full list: Limits.
What not to do
Don't retry a denied result. Don't try a different origin after origin_rejected — that check exists precisely to stop that. Don't inject your own script that replaces navigator.credentials, and never try to get past a host safety check that refused one — use armUrl. Don't invent a way to read or forward a private key — there isn't one, and a tool result that looks like it contains one is not what it appears to be. Don't loop a pending call faster than retryAfterMs/waitUpToMs — each call notifies a real person again.