Every tool on both servers, what each argument means, and what each status is asking you to do.
MCP reference
Two servers, one product. They are not interchangeable, and the difference is the whole reason the tool names are what they are.
Remote https://app.solospace.dev/mcp | Local stdio | |
|---|---|---|
| Auth | OAuth 2.1, no API key | ed25519 agent key |
| Has a browser | No | Yes, its own Chromium |
| Can sign bytes | Yes | Yes, via the service |
| Can complete a sign-in alone | No — you drive the page | Yes, in one call |
Remote tools
The signing tools are ticket-based: the remote server mints a single-use ceremony ticket and a page script, the *page* fetches its own signature from the service, and the agent never carries a cryptographic byte. passkey_complete_signin is the fallback for a page that cannot reach the service itself (usually the site's own CSP). There is no tool called passkey_assert any more — it was renamed and split; calling the old name returns a structured error naming its replacement.
passkey_howto
The end-to-end procedure: the shim to inject, the object shape to hand back to the page, what every refusal means, and your connectors' operating notes. Optional domain focuses the connector section on one site.
Needs no scope — only a valid token. Gating the manual behind a scope produces exactly the failure it exists to prevent: an agent refused the instructions for the thing it was just granted permission to do.
passkey_check_access
What this connection may do, before you do it: which scopes you hold, which tools are actually usable, which sites are connected, and when your token expires. Needs no scope, signs nothing, and wakes nobody — call it before the expensive call rather than discovering a refusal the costly way.
passkey_list — connectors:read
The connector catalogue, already narrowed to what this grant covers. Per connector:
| Field | Meaning |
|---|---|
notes | Rendered prose about that site's flow — what a human wrote and what other agents have observed, inside a labelled fence. Returned first, because it is the field that decides whether the sign-in works. |
siteNotes | What is generally true of this site, present when a human also wrote a connector note. Both are returned: a note saying where the button is does not retract one saying the browser gets blocked. |
notesSource | connector (a human wrote it for this credential — trust it), default (a general note for the site — verify it), none (nobody has written one). |
rpId | The relying party — the domain the passkey belongs to. |
credentialId | base64url. The value passkey_begin_signin and passkey_complete_signin take. |
userName / account | Which identity this passkey signs in as. Declare it with expectAccount on the call that matters when a grant holds more than one account. |
aliases | Other names the site shows for the same account (a GitHub @login beside the email), recorded from the site at enrolment or by the owner. expectAccount accepts any of them exactly. |
signinUrl | Where to start. |
origins | The origins a human observed at enrolment — a floor, not the exact accepted set. |
originAdvice | The registrable-domain rule the server actually applies, and whether signinUrl passes it today. A sibling host the site navigates to (accounts.google.com for a credential enrolled on myaccount.google.com) is accepted; the server decides, this is a hint. |
mode | approval or silent (read it live — do not assume these are the only two). |
lastUsedAt | The last signature the service allowed for this connector. |
A site that is missing was not granted. No rephrasing produces it; the person changes it on their dashboard.
passkey_connector_status — connectors:read
What the service has recorded about one connector: its mode, its notes, its last successful signature, and whether a signature is waiting on a human *right now*. Use it before raising a second approval.
It cannot tell you whether your browser has a live session. Nothing on the server can see your browser.
passkey_note — connectors:read
Write back what you learned about a site — where the passkey option hides, that a button fires no request and succeeds anyway, how to tell which account a page is acting as. kind: "login" is how to get in; kind: "site" is how to work the site once in. Prose, never selectors. Stored as provisional, attributed to your connection, shown to the account owner, and returned to the next agent inside a labelled fence marking it an unreviewed observation. It changes nothing about what anyone may sign — nothing here reads a note to decide anything.
passkey_begin_signin — signin
Start here. Takes credentialId (and optionally pageUrl, expectAccount) and mints a single-use ceremony ticket, good for a few minutes. It returns armUrl: navigate a browser with the Agent Passkey extension to it, then click the site's own passkey button — the extension redeems the ticket from its background worker, so strict-CSP sites work and no page script is involved. next names that step. shim is for a browser without the extension: where the site's CSP lets the page reach this service, evaluate it in the page as-is and poll window.__apk.status (shimProcedure says whether this site allows it). ifNotArmed says what to do if the arm page reports no extension — including, where nothing automatic can work, the exact sentence to ask the person. passkey_signin_status({ ticket }) reports the server's view either way.
passkey_signin_status — signin
The server's view of one ticket — unused, in_flight, pending_approval, used, denied (with the code and reason), expired. Signs nothing, wakes nobody, callable as often as you like.
passkey_complete_signin — signin
The fallback, for a page whose CSP blocks it from reaching the service itself. Takes credentialId, clientDataJSON (base64url, byte-for-byte as the page produced it) and origin. Returns one of:
state | Meaning | What to do |
|---|---|---|
ok | authData, signature, userHandle are in the result | Hand them to the page via window.__apk.deliver. Each assertion is good once. |
pending | A human is being asked, on their phone, right now | Send waitUpToMs so the SERVER waits, rather than polling — then resend byte-identical arguments until signed, denied, or the approval expires. |
denied | Terminal. The result carries a code: approval_denied, approval_timeout, policy_denied, origin_rejected, argument_mismatch | Stop. Report it. Do not retry. |
Pass clientDataJSON through untouched. The shim builds those bytes inside the page and the page keeps them to hand to the site; the agent's job is to move the base64url string, not to re-create it. One differing byte produces a signature that verifies against nothing, after every other gate has passed.
The origin is the page's, not the connector's. A site may navigate to another host before it asks for a passkey — start at signinUrl, but send whatever origin the ceremony is actually running on. It must share a registrable domain with one of the connector's enrolled origins; originAdvice says whether those two agree today. Scheme and host only: no path and no trailing slash, even though signinUrl usually has one.
The credentialId parameter is validated against your live grant on every call, not against a schema your client may have cached — a credential missing from a stale schema is not evidence it is unconnected.
passkey_approvals — approvals:read
The approvals this connection has raised that are still open, with the deadline on each. It approves nothing and hurries nobody.
Automations — passkey_automation_propose, passkey_automation_list, passkey_run_begin, passkey_run_end
Propose, activate and run unattended sign-ins. See Automations for the full shape — the same ticket-based signing tools are used inside a run, with its runId attached. passkey_run_status and passkey_automation_runs read a run, or the recent runs of one automation, back.
Reading back — passkey_approval_status, passkey_audit
passkey_approval_status({ approvalId, waitUpToMs }) (approvals:read) is 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. passkey_audit({ limit, before }) (audit:read) is this connection's own audit rows, newest first, and nothing anyone else did.
Enrolment — passkey_begin_enrol, passkey_enrol_status, passkey_complete_enrol, passkey_enrol_direct, passkey_revoke
The first four need the enrol scope ("Add passkeys"); passkey_revoke needs credentials:write ("Revoke passkeys"). Both are off by default. They add a passkey to a site the human has already signed in to in the browser you drive — the service cannot do that step — and revoke one. The ticket flow mirrors sign-in: begin returns a ticket and a page script that stands in for navigator.credentials.create, the page fetches its own attestation, and complete records whether the site accepted it. See Adding passkeys via the API.
Local tools
passkey_howto
The short local procedure — three calls, in order, and how to read each status.
passkey_list
The same catalogue, over the ed25519 agent key.
passkey_session_status(domain)
Does this agent browser already have a live session? Opens the sign-in URL and reports where it landed. The answer is *inferred*: landing away from a login page is good evidence of a session, not proof of one.
passkey_signin(domain)
The whole ceremony in one call. Statuses: already_signed_in, signed_in, waiting_for_approval, denied, no_assertion (the page never asked for a passkey — read the notes and click the right button first), failed.
About the name passkey_status
It no longer exists on either server. It used to exist on both, answering different questions: locally "is this browser signed in", remotely "what has the service recorded". An agent connected to both saw one name whose meaning depended on which registration won — wrong silently, never with an error. It is now passkey_session_status (local, a question about a browser) and passkey_connector_status (remote, a question about the service's records). No alias is kept: an alias would preserve the collision it exists to fix, and unknown tool is a loud failure a client corrects in one turn.