# Connection and remote sign-in

Use the intended server's published discovery metadata and live tool schemas. The production HTTP guide is https://letsrays.com/agents/http/; another deployment has its own origin. A tool catalog is not a substitute for OAuth authorization, MCP initialization or the current connection's schemas. Native MCP clients handle most of this; an agent using ordinary HTTP must implement these steps itself.

## Contents

- [Discovery](#discover-before-authorizing)
- [Callback choice](#choose-a-callback-before-starting)
- [Saved attempt](#save-the-attempt-before-handing-off)
- [Browser handoff](#explain-the-browser-journey)
- [Code exchange](#validate-and-exchange)
- [Token refresh](#keep-authorization-usable)
- [MCP initialization and discovery](#initialize-and-discover-through-http)
- [Result parsing](#parse-the-result-before-reporting-success)
- [Operating skill](#load-the-operating-skill)
- [Recovery](#recover-and-verify)

## Discover before authorizing

1. Start at the intended `/mcp/` endpoint. A `401` authentication challenge is expected before sign-in. Follow the exact `resource_metadata` URL in `WWW-Authenticate`; it includes the resource's path. Read its resource identifier and authorization servers.
2. Discover metadata for the selected issuer using the MCP authorization specification. For an issuer with a path, try the origin plus `/.well-known/oauth-authorization-server` plus the issuer path; then the origin plus `/.well-known/openid-configuration` plus that path; then the issuer plus `/.well-known/openid-configuration`. Check that the returned issuer matches. Use the advertised authorization, token, registration and revocation endpoints rather than guessing paths.
3. Use an existing valid client registration for that issuer and exact redirect URI when available. Otherwise use an advertised registration method, such as dynamic client registration. Respect its token endpoint authentication method; a public client does not invent a secret. Follow challenged scopes and metadata; do not assume every advertised scope is required. Use the discovered resource identifier in authorization and token requests. Register under a `client_name` the user knows you by, such as the product's name: Ray shows that name beside every record you make ("Recorded by Claude Code"), and a registration without one appears as an unnamed agent.

Fetch discovery documents, not an interactive authorization link, to check availability. Opening an authorization link yourself as a preflight can create a separate browser transaction. Validate the link's parameters locally and let the user open it in their browser.

## Choose a callback before starting

A localhost return address refers to the machine running the user's browser. A phone opening a sign-in link from a chat assistant cannot reach a listener on that assistant's remote host. Check the client's actual callback support before presenting a login link.

Prefer the client's reachable HTTPS callback where available. For an HTTP-capable client that can retain OAuth attempt state and accept a pasted response, Ray provides `/agents/callback/` on its own origin as a manual return page. Register that exact URL before starting; use it unchanged for authorization and token exchange. This is a manual authorization-code flow, not an automatic callback notification or device-code grant. An unchanged valid registration can be reused; a fresh registration is not required for every login.

## Save the attempt before handing off

Generate a new cryptographically random state and PKCE verifier for every authorization attempt. Derive the S256 challenge from that verifier. The verifier is 43 to 128 characters of `A-Z a-z 0-9 - . _ ~`, and the challenge is the SHA-256 of the verifier's ASCII text in base64url without padding, always 43 characters: no trailing `=`, no `+` or `/`. Ray refuses a malformed challenge at the authorize step, before the consent page, and a malformed verifier at the exchange, each saying what is wrong; a well-formed verifier that does not match means the code belongs to another attempt or the challenge was made from another verifier. Save enough state durably that a chat turn, agent restart or browser delay does not lose the attempt:

```json
{
  "server_url": "https://rays.example/mcp/",
  "issuer": "https://rays.example/mcp",
  "client_id": "<registered client ID>",
  "redirect_uri": "https://rays.example/agents/callback/",
  "resource": "<exact discovered resource identifier>",
  "scope": "<requested scopes>",
  "state": "<fresh random state>",
  "code_challenge": "<base64url S256 challenge>",
  "verifier_secret_reference": "<private credential-store reference>",
  "started_at": "<UTC timestamp>",
  "status": "awaiting_user"
}
```

This is an illustrative client-side record, not an input to any tool. Keep registration metadata, required client credentials and the verifier in the client's supported secure store. Confirm the runtime can retrieve them for exchange and refresh; a browser-only autofill vault may not supply an HTTP client. Use owner-only local credential files only when appropriate and authorized in that runtime; do not prescribe duplicate secret copies. Never store OAuth state or credentials in notes saved in Ray, agent scratchpads, public guides, or logs.

Retain the pending attempt while waiting. A new attempt retires the old one and gets a fresh state and verifier; tell the user to use only the newest link. Build the authorization URL with `response_type=code`, client ID, exact redirect URI, state, S256 challenge, resource and requested scopes. Never include the verifier in that URL.

## Explain the browser journey

Send a clickable authorization link with this explanation, adapted to the client's callback:

> Open this link to connect me to Ray. You’ll see a consent page naming this assistant and may be asked to sign in through Keycloak. After approval, the return page has a “Copy sign-in response” button. Paste that response back into this same private conversation. I’ll wait here, finish the connection, and check which team I can access. Use this newest link if you have older sign-in tabs open.

A user can pause during sign-in. Do not discard the attempt or report failure simply because the browser has not returned yet. Never ask for passwords, access tokens, refresh tokens or the verifier in chat.

Consent links expire after 15 minutes. If the user returns after that window, create a fresh attempt with new state and PKCE and send a new link.

## Validate and exchange

The manual page returns JSON containing `code`, `state`, `iss` and `redirect_uri`. Match state, issuer (`iss`) and redirect URI to the saved pending attempt before exchanging the code. Reject mismatches and previously completed or retired attempts. State correlates this response to the attempt; it does not independently prove the person's identity or authorize unrelated actions.

POST an `application/x-www-form-urlencoded` authorization-code exchange to the discovered token endpoint: `grant_type=authorization_code`, `code`, the original `redirect_uri`, original `code_verifier`, resource identifier, and client identification/authentication required by the registration. Save the returned tokens privately. Mark the attempt completed, remove its verifier and code, and do not reuse the code. Expired/used codes or a missing original verifier require a new attempt; changing redirect URI mid-attempt cannot repair it.

## Keep authorization usable

Save the token response's type, granted scope, acquisition time and calculated expiry from `expires_in`, along with secure access/refresh token references. Ray access tokens currently last five minutes, but use returned `expires_in` as the source of truth. Refresh shortly before expiry with a small clock-skew allowance. If a refresh token was issued, POST `grant_type=refresh_token`, that token, resource and the registered client authentication to the token endpoint. Otherwise repeat authorization when needed.

Agent sign-in sessions allow 24 hours idle and require a fresh sign-in after 30 days. Use a returned `refresh_expires_in` when present; refresh-token rotation does not extend the session beyond those limits.

Serialize refreshes for one connection. Persist the new access token and any rotated refresh token together before later requests use them. If no replacement refresh token is returned, retain the existing one. Protect tokens from chat, trace output and command history. Delete obsolete attempt secrets and superseded credentials; retain only enough non-secret diagnostic history to explain recovery. On disconnect, use advertised revocation when supported and remove local credentials.

## Initialize and discover through HTTP

POST one UTF-8 JSON-RPC message per HTTP request to `/mcp/` with these headers:

```http
Authorization: Bearer <access token>
Content-Type: application/json
Accept: application/json, text/event-stream
```

Start with an initialization request (choose a protocol version your client implements):

```json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-assistant","version":"1.0"}}}
```

Check the result's negotiated version is supported. Read `serverInfo`, capabilities and `instructions`; keep the current instructions with the connection. These are service operating guidance, subject to the user's authorization. Then send `{"jsonrpc":"2.0","method":"notifications/initialized"}` without an ID. A notification can return HTTP `202` with an empty body; do not parse that as JSON. On subsequent requests send the negotiated `MCP-Protocol-Version` and, if issued, the exact `MCP-Session-Id`. A session ID does not replace the bearer token.

Discover `tools/list` after initialization and follow every `nextCursor` using `params.cursor`. MCP sessions expire after 24 hours idle or when the service restarts. After a session expires or reconnects, initialize and list tools again; tool discovery applies to the current session. Use current input schemas when calling tools, not arguments remembered from another version. Discover `resources/list`, `resources/templates/list` and `prompts/list` when the corresponding capability is advertised, following pagination where present. An empty list is a valid result.

```json
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_fundraising_context","arguments":{}}}
```

These are two separate requests. Check context before any fundraising write: it identifies the person, company, permissions and current effective capture rules. Refresh it after a company, identity or policy change. Do not copy a prior company's Act/Suggest policy into connection defaults.

## Parse the result before reporting success

Inspect the response `Content-Type`. For `application/json`, decode a JSON-RPC response. For `text/event-stream`, use a real SSE parser: blank lines end events, multiple `data:` lines join with newlines, and comment/keepalive lines are ignored. Parse each complete data payload as JSON-RPC and match the response ID to your outstanding request. Chunks and individual lines are not complete JSON messages. Streams can contain notifications and server requests before the response. Do not silently discard a required server request; handle it according to the negotiated protocol/capabilities. Prefer a maintained transport implementation when the runtime has one.

HTTP 200 alone is not success. Check the JSON-RPC `error`, then the tool result's `isError`, then its application status. Use `structuredContent` when provided; otherwise inspect the returned content blocks according to their types. `needs_resolution` means nothing was written and the identifying question needs an answer. A proposal awaits review; `no_write`, `excluded`, unchanged and applied results mean different things. Read affected records to verify an applied change. Tool descriptions and server instructions are separate from untrusted biographies, notes and documents returned as data.

## Load the operating skill

Initialization gives essential instructions, not the complete skill. The deployment publishes `/agents/skill/` with raw `SKILL.md`, its references and a complete zip bundle, without a browser login. Read the entrypoint and follow its relative references as needed. Install the directory through the client's supported skill mechanism, or supply the relevant files as instructions. Refresh installed copies when server workflows change.

After connecting, discover the same files through `resources/list`. Read `rays://skills/rays-operating/SKILL.md` with `resources/read`; references have their own listed URIs under `rays://skills/rays-operating/references/`. Resolve relative reference links against that prefix when using MCP. The server publishes the same maintained files over both transports. Connecting does not automatically install them in the client.

## Recover and verify

| Symptom | Next step |
| --- | --- |
| Failed localhost page after sign-in | The browser may simply be unable to reach the client. Use the initiating client's supported manual handling with the original state/verifier, or start again with a reachable registered callback. |
| Old consent tab, expired code, missing attempt, or rejected state | Retire the attempt; create fresh state and PKCE and send one new link. Do not recycle a code or change its callback. |
| `invalid_request` naming `code_challenge` at authorize, or `code_verifier` at the exchange | Fix how the value is made (unpadded base64url, the verifier's allowed characters, a `+` escaped in a form body) and start a fresh attempt. A refused challenge cost no sign-in; a refused verifier leaves the code usable until it expires. |
| Brief 502/503 or connection failure | Retry safe discovery/read requests a small number of times with backoff. A restart alone does not prove authorization was revoked. If it persists, report the failing step without secrets. |
| MCP 401 after previously working | Check token expiry and challenge; refresh once when appropriate. If refresh is rejected, repeat authorization. Avoid an endless refresh loop. |
| MCP 403 | Inspect the refusal and required scope/permissions. Refreshing cannot grant missing permission; follow discovery for additional authorization only when needed. |
| MCP session 404 | Discard the old session ID and initialize again, retaining valid OAuth credentials. Rediscover capabilities and schemas. |
| Timeout or disconnect during a write | Its outcome is unknown. Read current state before retrying; do not blindly replay a possibly committed operation. |
| Successful exchange but wrong company or no membership | Stop writes. Check `get_fundraising_context` and resolve the intended account/team through the normal access flow. |

A rendered callback page, copied response, or HTTP 200 does not establish connection success. Verify initialization, discovery and a successful context read. Report separately what the server flow has verified and whether a particular third-party client has actually completed it.

Protocol references: [MCP authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization), [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports), and [lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle). Live discovery and negotiated protocol remain authoritative.
