# Contracts that must not break (/loops/contracts)
The recent blank-response bugs were not isolated rendering mistakes. They were contract failures between product identity, routes, conversation records, skill metadata, engines, and clients.
These invariants make those boundaries explicit.
Route, host, engine, session, and client may change. The Loop ID, dedicated skill binding, and artifact history must remain connected.
Every run that depends on a skill or outcome contract carries the skill ID, version, content digest, and outcome specification it actually executed.
Success, refusal, failure, cancellation, and timeout each leave a visible, comprehensible chat state. A client must never infer completion from a private event or silently filter the only result.
Conversation and message identity are canonical, versioned, and migration-safe. Engine sessions are rebuildable caches, not the source of truth.
A merge, deploy, heartbeat, queue completion, or receipt alone is not product proof. The same request must correlate through execution, artifact storage, client fetch, and the visible result.
## Required terminal states [#required-terminal-states]
| State | Durable record | What the person sees |
| --------- | ------------------------------ | ------------------------------------- |
| Completed | Result and outcome evidence | The useful result and what changed |
| Refused | Reason and unmet contract | A clear explanation, not a spinner |
| Failed | Error class and correlation ID | A plain failure with a safe next step |
| Timed out | Deadline and last known stage | A finished timeout state |
| Cancelled | Actor and time | Confirmation that work stopped |
## Compatibility rule [#compatibility-rule]
Server additions must be additive for supported clients. Protocol changes need explicit versions, migrations, and fallback rendering. A product flow is not complete until an old supported client can reach a safe terminal state.
## The proof ladder [#the-proof-ladder]
1. **Code:** the contract exists and has tests.
2. **Deploy:** the intended artifact reached the target environment.
3. **Runtime:** the active worker has the right version, digest, and configuration.
4. **Execution:** the request completes with correlated records and receipts.
5. **Experience:** the person sees the result in the client they use.
Map each contract to the implementation.
Read the failures that shaped these invariants.
# Developer reference (/loops/developer-reference)
Start with the product invariant:
> **specific Loop → dedicated skill → repeated outcome**
Then trace the request through the records and tools below.
## Source map [#source-map]
| Concern | Canonical starting point |
| ---------------------------------- | ------------------------------------------------------------------------ |
| Loop record and durable items | `packages/db/src/schema/loop.ts` |
| Dedicated skill binding | `packages/db/src/schema/loop-skill.ts` |
| Platform conversation kernel | `packages/db/src/schema/conversation.ts` |
| Conversation service and contracts | `apps/api/src/services/conversations.ts` and `conversation-contracts.ts` |
| Outcome contract and receipt | `packages/schemas/src/loop-outcome.ts` |
| Task, lease, and attempt records | `packages/db/src/schema/agent-tasks.ts` |
| Dispatch and matching | `apps/api/src/services/task-queue.ts` and `task-matchmaker.ts` |
| Surface contract and blocks | `packages/schemas/src/loop-surface.ts` and `loop-surface-block.ts` |
| Surface composition | `packages/db/src/loop-surface/` and `apps/mcp/src/tools/loop-compose.ts` |
| Mobile surface rendering | `apps/mobile/components/loop-surface/` |
| MCP Loop authorization | `apps/mcp/src/tools/` and `apps/api/src/routes/loops.ts` |
## Loop tool surface [#loop-tool-surface]
| Tool | Scope | Responsibility |
| ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `loop_read`, `loop_members` | `loops:read` | Resolve an existing Loop and its membership before acting |
| `loop_create` | `loops:write` | Create a chat-origin Loop with its exact title, renderer, dedicated skill ID, and already-earned seed items |
| `loop_pin_skill` | `loops:write`, hosted runtime | Pin the write-once Loop soul at birth; it returns only its version and digest |
| `loop_write` | `loops:write` | Persist or update an earned artifact with reference-only grounding where available |
| `loop_compose` | `loops:write`, hosted runtime | Replace presentation only when a supported surface materially improves the artifact |
| `loop_share`, `loop_unshare` | `loops:write` | Manage membership; item visibility does not grant access |
| `loop_set_item_visibility` | `loops:write` | Change the privacy of an agent-authored item without changing membership |
| `loop_archive` | `loops:write` | Remove the whole Loop from the active shelf without deleting history |
| `run_outcome` | `loops:write`, run-bound | Select the semantic terminal mode allowed by the current run contract |
## Engineering checklist [#engineering-checklist]
* Does the request preserve one Loop ID through every route and engine?
* Is the dedicated skill pinned by ID, version, digest, and outcome spec?
* Is the MCP token granted `loops:read` or `loops:write`, and is a hosted-runtime-only operation being attempted from the right principal?
* Does each terminal state produce a durable record and a visible chat result?
* Can a retry replace an attempt without changing the task’s meaning?
* Can the host detach without taking durable context with it, while the run keeps the exact skill release and authority it used?
* Does a fleet claim match a freshly measured capability instead of relying on host configuration or a heartbeat alone?
* Does the surface use only blocks exposed by the live contract?
* Does the proof correlate from request to rendered experience?
## Tests worth running [#tests-worth-running]
Use the narrowest test that proves the changed layer, then add end-to-end proof when the change crosses a boundary.
```bash
pnpm -F @ampersand/schemas test
pnpm -F @ampersand/db test
pnpm -F api test
pnpm -F mcp test
pnpm loop:gtkm:test
```
Package names and scripts can change. Confirm the current workspace scripts before running a copied command.
Return to the execution ownership model.
Review the five invariants before changing a boundary.
# How work runs (/loops/execution)
Legacy Loops use the deployed compatibility lane. New executable product work uses Capability Runs; see `docs/architecture/capability-run-execution.md` in the repository.
## Current Loop path [#current-loop-path]
1. An andee opens or sends into a Loop, or taps the supported web-discovery action.
2. `apps/api` loads the Loop's exact pinned Skill release and seals one turn envelope.
3. The in-process engine receives only the turn-scoped tools and authority.
4. Product writes and the `loop_runs` / `turn_log` evidence commit in PostgreSQL.
5. The request ends. There is no fleet host, task queue, VM, or unattended Loop successor.
`createLoop` does not enroll a schedule. `/v1/internal/raw-signals/drain-all` registers no `loop_turn` handler. Migration 0314 removes rows left by older deployments. `self_insights` remains a separate non-Loop schedule.
## Ownership line [#ownership-line]
| Layer | Owns |
| ------------------- | ------------------------------------------------------------------ |
| Loop | Intent, members, pinned Skill, items, and conversation identity |
| Turn envelope | Exact context, grants, model policy, and trigger for one attempt |
| In-process engine | Model execution and tool choice for that turn |
| PostgreSQL evidence | Run state, turn state, effects, and operator-visible failure facts |
Do not rebuild the retired fleet/orchestrator lane. Do not turn stored `loops.cadence` metadata into a clock. A later legacy Loop pass requires another explicit andee action.
# How it appears (/loops/experience)
People should not need to understand the machinery. They begin with an ask, see useful work, and return to something that remembers.
A Loop can appear in three ways:
| Approach | Best when | The Loop remains |
| ---------------- | ------------------------------------------------------------ | ------------------------------------- |
| Conversation-led | The next move depends on dialogue or clarification | The system coordinating the work |
| Surface-led | A structured artifact is the clearest thing to review or use | The Loop that produced the artifact |
| Hybrid | Dialogue creates or refines a structured outcome | The shared identity across both views |
## Route is not identity [#route-is-not-identity]
A route answers “where should the app open?” It does not answer “what is this product object?”
Starter entry points and the generic Loop view may use different routes. They still must resolve to the same durable Loop identity. Changing a route must not fork the conversation, hide a result, or create a second Loop.
## Transcript is not the Loop [#transcript-is-not-the-loop]
A conversation is an interaction record. It may be rebuilt, migrated, or shown in more than one client. The Loop coordinates the purpose, context, membership, skill binding, and work. Its artifacts are the durable outcome history.
## Surface is a presentation choice [#surface-is-a-presentation-choice]
The agent chooses the clearest renderer supported by the live contract. A surface can compose available blocks when that improves the current artifact. It is optional, capability-gated, and never invented when the tool or block does not exist.
Conversation, surface, and route must point back to the same Loop.
Every terminal turn must leave a state that a person can understand.
A new server response must remain legible to supported clients. Filtering in one client cannot define product success.
## What the andee should feel [#what-the-andee-should-feel]
* “This is still the thing I started.”
* “It remembers the useful parts.”
* “I can see what happened.”
* “I know what I can do next.”
Follow the work from durable context to a replaceable engine.
See how route and transcript drift produced blank experiences.
# How a Loop grows (/loops/growth)
A Loop begins when an andee wants ongoing work around one specific intent. **Family movie night**, **Best tacos in KC**, and **My reading list this year** are Loops. “Family,” “food,” and “books” are not.
The simple test is:
> **specific Loop → dedicated skill → repeated outcome**
The Loop is visible. Its skill is hidden. The skill can call tools and use runtime capabilities to turn relevant evidence into an artifact. That artifact is the durable outcome of the pass.
## Birth [#birth]
### Reuse before create [#reuse-before-create]
Read by exact title and skill ID. Reuse a match. Otherwise create the Loop with its exact visible title, a renderer selected from the live contract, the dedicated skill ID, and only durable starter items already earned by the request.
### Ground in what is known [#ground-in-what-is-known]
Use approved identity signals already in context. For other people named in the Loop, use `ask_question` with those people as participants before making claims about them. That reads identity; it does not send them a message.
### Make one coherent batch [#make-one-coherent-batch]
When evidence is rich and on-topic, produce a bounded artifact that a person can absorb in one sitting. It may be one result, a short set, or a grouped synthesis. Every part must tie back to evidence. Use reference-only grounding and provenance when the live contract accepts it; never put private raw evidence in an artifact.
### Be honest about thin evidence [#be-honest-about-thin-evidence]
Sparse evidence means no write. Net-noisy evidence may justify one sharp question when the answer would change the artifact. Neither case justifies filler.
### End visibly [#end-visibly]
Finish the birth turn with a clear chat reply that explains the result—or the honest no-write conclusion.
## What never belongs in a Loop [#what-never-belongs-in-a-loop]
* A “Loop started” item
* A progress or checkpoint item
* An opaque cursor or batch marker
* A fabricated artifact made only to prove the Loop exists
The Loop existing is not an artifact. Artifacts must carry useful, durable value.
## Deepening [#deepening]
Each useful pass reads the existing artifacts, deduplicates against them, and adds or refines the next meaningful result. A large corpus becomes several coherent passes, not one overwhelming dump.
The agent chooses the honest output budget and the clearest available presentation. It can use conversation, an existing renderer, or an authorable surface when the live tool contract supports one. It should respect an existing Loop’s renderer and surface.
Separate the durable Loop from the route and screen used to reach it.
See the guarantees that make birth and return safe.
# Incidents and evolution (/loops/incidents)
This appendix records why the guide says what it says. It is context, not the product definition.
## Blank response after a successful run [#blank-response-after-a-successful-run]
An older transcript shape and a newer client route disagreed about which messages belonged to the active Loop. The server completed work, but the client filtered the useful response. The person saw a blank state.
**What it exposed:** route and transcript conventions had become implicit identity.
**What changed:** conversation identity moved toward a platform-owned kernel; engine sessions became rebuildable execution caches; visible completion became an explicit contract.
## Birth stuck after the conversation-kernel change [#birth-stuck-after-the-conversation-kernel-change]
Get to know me and Connect births could stall after dispatch. The run did not carry enough immutable skill metadata to resolve its outcome contract. The system refused the outcome, while retry behavior left the client looking blank or stuck.
**What it exposed:** a skill name was not enough provenance, and a terminal refusal hidden in execution records was not a product result.
**What changed:** runs must pin skill ID, version, content digest, and outcome specification; every terminal state must return a visible response.
## The rideshare direction [#the-rideshare-direction]
Michael’s proposal separated three concerns:
* **Workspace:** durable identity and context
* **Task:** one unit of execution
* **Host:** replaceable compute that checks work out and returns it
The team refined that boundary:
* Loops own intent, context, and artifact history.
* Tasks own execution.
* Attempts own retries.
* Adapters own provider mechanics.
* Specialized ledgers keep domain evidence.
This keeps a Loop stable while engines, providers, and host infrastructure change.
## Ideas that remain hypotheses [#ideas-that-remain-hypotheses]
Earlier materials explored “Loop as an operating system,” a building-block economy, broad capability composition, and marketplace-like reuse. Those ideas may inform future strategy. They are not required to understand or implement a Loop today.
The canonical model stays small:
> A Loop is the user-visible system of work around one specific intent. It can use context, a dedicated skill, tools, conversations, surfaces, tasks, and engines. Its artifacts are the durable outcome.
Return to the product definition.
See the invariants these incidents produced.
# What is a Loop? (/loops)
A **Loop** is the user-visible system of work around a specific intent. It can carry context, use a dedicated skill, call tools, run tasks, hold a conversation, and render a surface.
Those parts help the Loop do its work. They are not the outcome.
> **The artifacts are the outcome.** They are the useful results that remain after a run, route, host, or engine has changed.
```text
Loop
├── context
├── dedicated skill
├── conversation and surfaces
├── tool calls
├── runs, tasks, and attempts
└── artifacts ← durable outcome
```
## What changes and what stays [#what-changes-and-what-stays]
| Can change | Stays with the Loop |
| ------------------------ | --------------------------------------------------- |
| Route or screen | Artifact history |
| Conversation turn | Earned facts and evidence in those artifacts |
| Tool calls | The relationship between each artifact and its Loop |
| Task attempt | Provenance needed to understand the outcome |
| Engine, adapter, or host | The useful result the andee can return to |
An artifact may be a recommendation set, plan, list, synthesis, profile, decision, or another keepable result. A later run can refine it or produce the next artifact. The Loop provides continuity around that work.
## Product rule [#product-rule]
Each starter Loop is one specific thing, backed by one dedicated skill. The skill can orchestrate any allowed tools and runtime capabilities. It exists to produce better artifacts for that Loop—not to become a visible product category.
Good examples include **Family movie night**, **Best tacos in KC**, **My reading list this year**, and **Get to know me**.
It contains the context and execution needed to pursue a specific intent.
They are the part an andee returns to after the machinery has finished or changed.
Routes, tasks, attempts, adapters, engines, and hosts can change without losing the artifact history.
## Continue [#continue]
See how context becomes a bounded, evidence-backed artifact.
Follow the work through tasks, attempts, adapters, and replaceable hosts.
Review the guarantees that preserve artifacts and visible completion.
Map the model to schemas, services, tools, and tests.
# Authentication (/mcp/authentication)
The MCP server is an OAuth protected resource. It verifies bearer tokens but delegates authorization to the & API.
## Discovery [#discovery]
| Document | Production URL |
| ----------------------------- | ------------------------------------------------------------ |
| Protected resource metadata | `https://mcp.and.com/.well-known/oauth-protected-resource` |
| Authorization server metadata | `https://api.and.com/.well-known/oauth-authorization-server` |
An unauthenticated protocol request returns `401` with a `WWW-Authenticate` header that points to the protected-resource metadata.
## Supported grants [#supported-grants]
* Authorization code with PKCE using `S256`
* Device authorization
* Refresh token
* Dynamic client registration for public clients
Bearer tokens must be sent in the `Authorization` header. Query-string and cookie tokens are not supported.
## Default public scopes [#default-public-scopes]
| Scope | Allows |
| ------------------- | ----------------------------------------------------------- |
| `signals:ask` | Synthesized identity answers and public andee discovery |
| `signals:write` | Durable identity writes and identity suggestions |
| `connections:read` | Connections, pending requests, profiles, and question state |
| `connections:write` | Connection changes and peer-question actions |
These four scopes form the default grant for public MCP clients.
## Hosted-runtime scope [#hosted-runtime-scope]
The protected-resource metadata also advertises these product scopes:
| Scope | Allows | Grant rule |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `loops:read` | Read the caller's Loops and members | Not part of the default public grant; request it explicitly |
| `loops:write` | Create, enrich, share, archive, and manage the caller's Loops | Not part of the default public grant; request it explicitly |
| `signals:evidence_read` | Read the caller's approved evidence and their resolved place set (never coordinates) | Reserved for the hosted & runtime |
| `signals:raw_read` | Legacy: gates a handful of `/v1/raw-signals` REST routes still used by the pre-engine hosted-fleet runtime. No current MCP tool requests it | Reserved for the hosted & runtime; carried only by that retiring path |
The & hosted runtime receives all four. Public authorization strips `signals:evidence_read` and `signals:raw_read` even when a client requests the complete advertised scope list. `loops:read` and `loops:write` are opt-in rather than default, and some writes remain first-party-only: authoring a Loop skill and composing a Loop surface require the andee's own hosted runtime. Every tool and API route still enforces its own scope and ownership rules.
## Enforcement [#enforcement]
Each tool advertises its OAuth scheme in MCP metadata and checks scope again inside its handler. API routes enforce the same scope when the MCP service proxies the call.
`questions_answer` requires both `connections:write` and `signals:write` because it updates a peer question and creates an identity signal. `questions_list` is advertised with `connections:read`; its handler also accepts `connections:write` for callers checking questions they were allowed to send.
Expired, malformed, or missing tokens return `401`. An authenticated call without the tool's required scope returns a tool error and should trigger reauthorization with the current scope set.
# Connections (/mcp/connections)
Connection tools expose names, tags, photos, relationship state, and request notes. They do not expose identity signals by themselves.
## Read the graph [#read-the-graph]
### `connections_read` [#connections_read]
Accepts an optional `limit` from 1 to 100. It returns two buckets:
* `active`: mutual connections, with name, `&tag`, photo, and connection date;
* `peopleYouMayKnow`: andees matched from phone contacts, marked `mutual` or `one_way`.
Use the active list to resolve a person's name before another connection action. If more than one person matches, ask the user to choose.
### `connections_pending` [#connections_pending]
Takes no arguments. It returns pending requests with `direction: incoming` or `direction: outgoing`. Only incoming requests can be accepted or denied.
## Change the graph [#change-the-graph]
| Tool | Arguments | Behavior |
| --------------------- | ------------------------------------------- | -------------------------------------------------------------------- |
| `connections_request` | `tag`, optional `note` up to 280 characters | Sends a new request and notifies the recipient |
| `connections_accept` | `tag` | Accepts a matching incoming request |
| `connections_deny` | `tag` | Declines a matching incoming request without notifying the requester |
| `connections_remove` | `tag` | Removes an active connection from both sides |
Tags may include or omit the leading `&`. Accept and deny first resolve the incoming pending list, so they return a clear no-match instead of guessing.
`connections_remove` is destructive. Both people lose access to connection-only signals, and reconnecting requires a new request. Call it only after the andee explicitly asks to disconnect.
# Identity and discovery (/mcp/identity)
## `ask_question` [#ask_question]
Returns a synthesized natural-language answer. It does not expose raw identity rows to public clients.
| Argument | Type | Required | Notes |
| -------------- | --------- | -------- | -------------------------------------------------------------------------- |
| `question` | string | Yes | Free-form question; maximum 1,000 characters |
| `participants` | string\[] | No | Up to 10 `&tag` or connected first-name references; the caller is implicit |
With explicit participants, the API loads only facts visible to the caller. Ambiguous names return a disambiguation result. With no participants, an audience question can rank the caller's full connection graph.
## `find_andees` [#find_andees]
Finds an andee outside the caller's existing connections by name and optional public-information clues.
| Argument | Type | Required | Notes |
| ------------- | --------- | -------- | ------------------------------------------------------- |
| `name` | string | Yes | One name or a full name |
| `constraints` | string\[] | No | Up to five public-fact phrases; every phrase must match |
The result is a single match, a bounded disambiguation set, or a clear no-match. Supporting MCP Apps clients can render the structured result as an andee card.
## `andee_read` [#andee_read]
Reads another andee's profile after server-side visibility filtering.
Pass exactly one argument:
| Argument | Type | Behavior |
| -------- | ------ | --------------------------------------------------------------------- |
| `tag` | string | Global lookup by `&tag` |
| `name` | string | Search the caller's connections; returns a profile only for one match |
Public signals are always eligible. Connected-only signals are available only to the andee's own first-party runtime when a deeper connection exists. Third-party OAuth clients receive public signals only.
## `identity_write` [#identity_write]
Keeps one stable fact the authenticated andee explicitly stated in & agent chat or a Loop chat turn.
| Argument | Type | Required |
| -------- | ------ | -------- |
| `value` | string | Yes |
Use it for durable preferences, relationships, places, work, interests, or values that the person said directly or explicitly asked the agent to remember. Send the person's complete exact sentence, never a fragment or paraphrase. The API matches it to the server-owned message for that signed chat turn and approves it immediately. Other MCP and CLI writes remain pending. Do not use it for inferred identity or temporary state; use `signal_suggestion_write` for an inference.
## `signal_suggestion_write` [#signal_suggestion_write]
Offers a structured identity candidate inferred during agent chat or learned during a Loop turn.
| Argument | Type | Required | Notes |
| ------------- | ----------------------------------- | -------- | --------------------------------------------------------------------------- |
| `draftedText` | string | Yes | Succinct, rich first-person identity claim with distinguishing detail |
| `hdsCodeHint` | string | Yes | Valid HDS leaf code |
| `loopId` | string | No | Required during a Loop turn; omitted in agent chat |
| `visibility` | `private`, `connected`, or `public` | No | Suggested privacy metadata for the pending review item |
| `context` | object | No | Flat scalar values or string arrays; no nested objects, coordinates, or PII |
The API verifies the active agent or Loop run, validates the HDS code and context size, and compares meaning against approved and pending identity plus every prior suggestion. Broad topics, noise, and semantic duplicates return `drafted: false` and create nothing. `drafted: true` means a pending mobile-review item exists; it does not mean the signal is already part of the \&tag.
# MCP server (/mcp)
The & MCP server gives compatible agents a secure tool layer over an andee's identity and relationships.
It supports four jobs:
* answer identity questions without returning private source rows to third-party clients;
* save durable facts and identity suggestions;
* discover and connect with other andees;
* ask, answer, and track questions between connected andees.
The production endpoint is:
```text
https://mcp.and.com/mcp
```
Every protocol request requires a bearer token. Compatible clients discover the authorization server from the MCP protected-resource metadata and complete OAuth before calling tools.
## Choose a path [#choose-a-path]
Add the remote server URL to an MCP client and sign in.
Review OAuth discovery, grants, scopes, and access rules.
See every current identity, discovery, connection, and question tool.
Review transport endpoints, request behavior, and errors.
## Architecture [#architecture]
```text
MCP client
└─ OAuth bearer token
└─ mcp.and.com/mcp
├─ verifies the token locally
├─ registers tools for that request
└─ proxies authorized operations to api.and.com
```
The MCP service does not query product data directly. Tool handlers forward the verified caller and bearer token to the API, which applies authorization and visibility rules.
## Public and hosted access [#public-and-hosted-access]
External OAuth clients receive the default public tool surface. Loop access is an explicit `loops:read` or `loops:write` grant, not a default permission. The hosted & runtime also receives Loop capabilities and evidence tools that read the authenticated andee's approved facts, photo evidence, and their resolved places. Evidence tools require `signals:evidence_read`, which public OAuth clients cannot obtain.
# Connect a client (/mcp/installation)
## Before you start [#before-you-start]
You need a claimed `&tag`, the & mobile app, and an MCP client that supports remote Streamable HTTP servers and OAuth.
Use this server URL:
```text
https://mcp.and.com/mcp
```
## Add the server [#add-the-server]
Use `and` as the local server name, or choose another name your client supports.
For clients that accept a shared MCP configuration:
```json
{
"mcpServers": {
"and": {
"type": "http",
"url": "https://mcp.and.com/mcp"
}
}
}
```
Some clients ask only for a name and URL. Enter `and` and the production URL above.
## Authorize the client [#authorize-the-client]
### Start sign-in from the MCP client [#start-sign-in-from-the-mcp-client]
The client follows the server's OAuth metadata and opens the authorization page on `and.com`.
### Scan the code with the & app [#scan-the-code-with-the--app]
Use the signed-in mobile app to scan the QR code. The app shows the requesting client and requested access.
### Approve access [#approve-access]
After approval, the browser returns to the client. The client exchanges the authorization code for an access token and refresh token.
### Verify the connection [#verify-the-connection]
Ask the client to list tools or call `ask_question` with a simple question about the signed-in andee.
If the scan fails, confirm that the phone is signed in to the same account and restart authorization from the client. If a tool reports a missing scope, remove the old connection and authorize it again so the client receives the current public scope set.
## Local development [#local-development]
The MCP service defaults to `http://localhost:3004`; the API authorization server defaults to `http://localhost:3005`.
From the repository root:
```bash
pnpm --filter mcp dev
```
Both services must share the access-token secret. See the contributor README beside the server implementation for local environment details.
# MCP Apps (/mcp/mcp-apps)
`find_andees` advertises this UI resource:
```text
ui://ampersand/andee-card.html
```
Hosts that support the MCP Apps UI extension can fetch it through `resources/read` and render the returned HTML in a sandboxed frame. The resource uses the `text/html;profile=mcp-app` media type.
The andee card renders match and disambiguation results from the tool's structured output. Its content-security policy allows images from the production photo bucket.
The UI is an enhancement, not a tool dependency. Hosts without MCP Apps support ignore the metadata and use the text result. If the widget bundle is missing when the server starts, the server skips the resource registration and the tool still returns its normal text and structured content.
# Protocol and errors (/mcp/protocol)
## Endpoints [#endpoints]
| Method | Path | Purpose | Authentication |
| ------ | --------------------------------------- | -------------------------------------- | ---------------------- |
| `POST` | `/mcp` | Streamable HTTP MCP protocol | Bearer token required |
| `POST` | `/` | Alternate protocol route | Bearer token required |
| `GET` | `/.well-known/oauth-protected-resource` | OAuth protected-resource discovery | Public |
| `GET` | `/.well-known/openai-apps-challenge` | Optional domain-verification challenge | Public when configured |
| `GET` | `/health` | Service health | Public |
Use `/mcp` for integrations. The root protocol route exists as a compatibility path.
## Stateless transport [#stateless-transport]
The server uses MCP Streamable HTTP without server-generated session ids. Each request creates a server and transport instance, registers the tools allowed by the verified token context, handles one JSON-RPC body, and closes with no transport session state.
Clients must send:
```http
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer
```
## Request validation [#request-validation]
* Missing or invalid bearer token: `401 invalid_token` with `WWW-Authenticate`
* Missing or non-object JSON body: `400 invalid_request`
* Unhandled server error: `500 internal_error`
* Tool input or downstream API failure: MCP tool error result
Tool handlers return errors as tool content where possible so the agent can explain or correct the request. The MCP service forwards the bearer token to the API, and the API remains the final authority for scope and visibility checks.
## Health [#health]
`GET /health` returns at least:
```json
{ "status": "ok" }
```
Health proves that the HTTP process can respond. It does not prove a particular tool call, token, or downstream API path succeeds.
# Peer questions (/mcp/questions)
Peer questions are asynchronous. Sending a question returns immediately; the answer arrives only after the recipient responds.
## Send and track [#send-and-track]
### `ask_andee` [#ask_andee]
| Argument | Type | Required | Notes |
| ---------- | ------ | -------- | -------------------------------------- |
| `tag` | string | Yes | Must identify an active connection |
| `question` | string | Yes | Asked verbatim; maximum 500 characters |
The recipient receives a notification and an in-app choice to answer or decline. Use this tool only when `ask_question` cannot answer from identity already visible to the caller.
### `questions_list` [#questions_list]
Takes no arguments. It returns every question the caller sent, newest first, with status `pending`, `answered`, `rejected`, `expired`, or `withdrawn`.
The list includes timestamps and the number of identity signals created from an answer, but not the answer text. Use `ask_question` after an answer becomes visible.
### `questions_withdraw` [#questions_withdraw]
Accepts `questionId`. It retracts a question only while it is pending and does not notify either side.
## Receive and respond [#receive-and-respond]
### `questions_pending` [#questions_pending]
Takes no arguments. It lists open questions addressed to the caller, including the question id, asker, text, and expiration time.
### `questions_answer` [#questions_answer]
| Argument | Type | Required | Notes |
| ------------ | ------ | -------- | --------------------------- |
| `questionId` | string | Yes | Id from `questions_pending` |
| `answer` | string | Yes | 1 to 500 characters |
Answering updates the question and writes the response into the caller's mobile identity-review queue. It requires both `connections:write` and `signals:write`. The asker receives a notification.
### `questions_reject` [#questions_reject]
Accepts `questionId`. It silently declines the pending question and does not create an identity signal.
# Hosted-runtime evidence (/mcp/runtime-evidence)
These tools require `signals:evidence_read`. Public OAuth clients cannot receive that scope. They expose evidence for the authenticated andee only; callers never pass a subject id.
## `identity_read` [#identity_read]
Pages through approved identity facts without synthesis.
| Argument | Type | Notes |
| ------------ | ------------- | ----------------------------------------------------------- |
| `snapshotAt` | ISO timestamp | Fixed snapshot from the first page; reuse it on later pages |
| `cursor` | string | Opaque continuation cursor |
| `limit` | integer | Defaults to 50; maximum 100 |
Follow `nextCursor` with the same `snapshotAt` until it is null when the task needs the full corpus.
## Photo evidence [#photo-evidence]
### `photo_evidence_read` [#photo_evidence_read]
Pages through safe, derived label evidence and extraction coverage using optional `offset` and `limit`. It never returns image bytes, asset ids, raw coordinates, or arbitrary metadata. The maximum page size is 100.
## Place evidence [#place-evidence]
### `place_evidence_read` [#place_evidence_read]
The andee's own resolved place evidence, not raw substrate. Filter and page with:
| Argument | Type | Notes |
| ----------------- | --------------------------- | -------------------------------------------------------------- |
| `since` | ISO timestamp | Only evidence observed at or after it |
| `until` | ISO timestamp | Only evidence observed at or before it |
| `confidenceFloor` | `low` \| `medium` \| `high` | Minimum confidence; filter on it when the task needs certainty |
| `category` | string | Restrict to one place category, e.g. `museum` |
| `offset` | integer | Row offset; start at 0 |
| `limit` | integer | Defaults to 50; maximum 200 |
Each returned row carries an observation key, the event's time, a `confidence` (`low` | `medium` | `high` — a dwell/accuracy score), the fact prose, the selected place, and the ranked candidates that were considered. The top-level `places` array is the deduped set of places behind those rows, so each place's description is returned once rather than repeated per row.
A place with `status: "pending"` is real and named — its description is still being written. Report what you know and pick the prose up on a later call; a pending status is an honest partial result, never an error, and never permission to invent a description.
No response from this tool contains a coordinate. The place name and the candidate distance are the meaning; the point is the substrate it was refined out of.
## `suggestion_feedback_read` [#suggestion_feedback_read]
Takes no arguments. It returns aggregate feedback grouped by HDS top level, primary type, and provenance so the runtime can avoid repeating weak identity suggestions.
## Privacy boundary [#privacy-boundary]
The protected-resource document advertises the complete MCP capability surface, but public authorization strips `signals:evidence_read` before issuing a token. Loop scopes are discoverable but opt-in; they are not part of the default public grant. The API validates issued public scopes again when an authorization code is redeemed.
# Tool catalog (/mcp/tools)
Tool calls run as the account that authorized the MCP connection. The server injects the caller from the bearer token; clients never pass an andee id.
## Public tools [#public-tools]
| Tool | Purpose | Scope | Changes data |
| --------------------- | -------------------------------------------------------------- | ------------------------------------- | ---------------- |
| `ask_question` | Synthesize an answer from the caller and optional participants | `signals:ask` | No |
| `find_andees` | Find a non-connection by name and public facts | `signals:ask` | No |
| `andee_read` | Read a visibility-filtered andee profile | `connections:read` | No |
| `identity_write` | Save one durable fact | `signals:write` | Yes |
| `connections_read` | List active connections and people the caller may know | `connections:read` | No |
| `connections_pending` | List incoming and outgoing connection requests | `connections:read` | No |
| `connections_request` | Send a connection request | `connections:write` | Yes |
| `connections_accept` | Accept an incoming request | `connections:write` | Yes |
| `connections_deny` | Decline an incoming request | `connections:write` | Yes |
| `connections_remove` | Remove an active connection | `connections:write` | Yes, destructive |
| `ask_andee` | Send a question to a connected andee | `connections:write` | Yes |
| `questions_pending` | List questions waiting for the caller | `connections:read` | No |
| `questions_list` | List questions sent by the caller | `connections:read` | No |
| `questions_answer` | Answer a question and create an identity signal | `connections:write` + `signals:write` | Yes |
| `questions_reject` | Decline a question | `connections:write` | Yes |
| `questions_withdraw` | Withdraw a pending question the caller sent | `connections:write` | Yes |
## Turn-scoped tools [#turn-scoped-tools]
These tools are registered on the MCP server but require the signed run token that & mints for the current & agent or Loop turn. A public OAuth connection cannot call them on its own.
| Tool | Purpose | Scope | Changes data |
| ------------------------- | ----------------------------------------- | --------------- | ------------ |
| `signal_suggestion_write` | Draft an inferred identity recommendation | `signals:write` | Yes |
## Hosted-runtime tools [#hosted-runtime-tools]
These tools require `signals:evidence_read`. Public OAuth clients cannot receive that scope.
| Tool | Purpose | Changes data |
| -------------------------- | -------------------------------------------------------------- | ------------ |
| `identity_read` | Page through a fixed snapshot of approved identity facts | No |
| `photo_evidence_read` | Page through safe photo-label evidence and extraction coverage | No |
| `place_evidence_read` | Read the caller's resolved places with enrichment descriptions | No |
| `suggestion_feedback_read` | Read aggregate identity-suggestion feedback | No |
## Tool selection [#tool-selection]
* Use `ask_question` for a synthesized identity answer.
* Use `identity_write` only for a stable fact worth remembering.
* Use `connections_read` to resolve a known connection before changing the relationship.
* Use `find_andees` to resolve someone outside the caller's connection graph.
* Use `ask_andee` only when stored identity cannot answer the question and the person is already connected.
* Treat `connections_remove` as destructive and call it only after an explicit request.
The detailed pages describe inputs, lifecycle rules, and visibility behavior.