# 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

### `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_list` — `connectors:read`

The connector catalogue, already narrowed to what this grant covers. Per connector:

| Field | Meaning |
|---|---|
| `notes` | Prose about that site's flow. 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_assert` takes. |
| `userName` | Which identity this passkey signs in as. |
| `signinUrl` | Where to start. |
| `origins` | The origins a human observed at enrolment. An assertion for any other origin is refused. |
| `originAdvice` | Whether `signinUrl` starts on an enrolled origin, in words. The sign-in URL is where you start; the ceremony origin is wherever the site has navigated to when it asks for the passkey, and only the second one is checked. |
| `mode` | `approval` or `silent`. |
| `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_assert` — `signin`

The product. Takes `credentialId`, `clientDataJSON` (base64url, byte-for-byte as the page produced
it) and `origin`. Returns one of:

| `status` | Meaning | What to do |
|---|---|---|
| `signed` | `authData`, `signature`, `userHandle` are in the result | Hand them to the page. Each assertion is good once. |
| `waiting_for_approval` | A human is being asked, on their phone, right now | Sleep `retryAfterMs`, then call again with **byte-identical** arguments — at most once per interval — until signed, denied, or the approval expires (~90s). |
| `denied` | Terminal. The result carries a `code`: `approval_denied`, `approval_timeout`, `policy_denied`, `origin_rejected`, `not_found` | 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. Do not decode and re-encode it, reformat the JSON or reorder its keys. The service
hashes what it is sent and the site hashes what the page holds; 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 be one of the connector's enrolled `origins`, and `originAdvice` says
whether those two agree. Scheme and host only: no path and no trailing slash, even though
`signinUrl` usually has one.

The `credentialId` and `domain` parameters are **enumerations built from your grant**, per
request. A credential you were not granted is not offered in the schema at all — and is refused by
the server as "no such credential" if you send it anyway, indistinguishably from one that does not
exist.

### `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.

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