# MCP servers that work well in Streamliner

For whoever writes or owns the MCP servers a streamline uses. Any standard MCP server works; these rules make it
fast, safe with secrets and honest about what happened. Use this list to review a server, and to tell its owner
what to change; never to work around a server's own rules.

## Contents
- Rules, one by one
- Result shape
- Errors
- Checklist

## Rules, one by one

1. **The API holds the rules.** Price floors, limits, who may see what: enforced in the API for every call, as the
   signed-in person. Assume the caller is a model that can be wrong or manipulated.
2. **One job, one handle.** The first tool that creates the job's object returns its id (`cartId`, `poId`) with
   `chat.set` so the stream keeps it; later tools take it as an argument. MCP 2026-07-28 is stateless.
3. **Annotate every tool**: `readOnlyHint`, `destructiveHint`, `idempotentHint`. "WRITE" in a description is not
   an annotation. Without annotations every call needs a tap (personal) or a hand-written policy.
4. **No secret in any `inputSchema`.** SSN, date of birth, licence, PIN, password, income for a credit check: return
   a sealed form (MCP form-mode elicitation naming a configured form, or the `sealedForm` directive). Cards not
   present: URL-mode elicitation to the processor's hosted page. Cards present: a terminal tool.
5. **Confirm-gated writes**: a read-only **preview** tool returning the real current state (the cart), the commit
   tool marked `destructiveHint: true`, and still validated and idempotent in the API.
6. **`direct` tools** take fully determined arguments, do one small thing, are safe to run twice.
7. **Completion**: the completing tool returns the final identifiers and `complete: true` (or is listed in
   `completion.tools`); `chat.status: "waiting"` while blocked on someone else; never report success not achieved.
8. **Sign-in errors are HTTP**: missing, expired or revoked credential → **401** with `WWW-Authenticate` (OAuth
   servers: `resource_metadata`); signed in but not allowed → 403. Never "please log in" as text. **An ended session
   is a 401, never a business code** (if a server answers `{ok:false, error:{code:"SESSION_ENDED"}}` with 200, a
   personal streamline can list that code in `reauthOn` as a stopgap).
9. **Act as the person**, never a service account; allow calls from phones (no IP-pinning to Streamliner).
10. **Idempotency**: honour `Idempotency-Key` on every write; the same key returns the same result with no second effect.

## Result shape

```json
{ "content": [{ "type": "text", "text": "Added the 1.52 ct solitaire (R-20417) at $4,850." }],
  "structuredContent": { "cartId": "C-1029", "item": { "sku": "R-20417", "price": 4850 } },
  "_meta": { "com.manolab.streamline/ui": { "card": { "template": "item.card", "data": "$.item" },
                                            "overview": { "refresh": true }, "chat": { "set": { "cartId": "C-1029" } } } } }
```

- `content`: one or two factual sentences for the model (paid for on every later turn).
- `structuredContent`: the data templates draw; keep its shape stable (it is a contract with the templates).
- Directives (the model never sees `_meta`): `card` / `cards` (`{template, data}`), `overview` (`{refresh: true}` or
  `{data}`), `chat.set` (≤ 20 string/number keys), `chat.status` (`open` \| `waiting` \| `done`), `sealedForm`,
  `sealedView`, `suggestions` (≤ 3 chips), `notice` (`{text, tone}`), `complete: true`. Unknown keys are ignored.
- Never send HTML for Streamliner (MCP Apps `ui://` resources are ignored in the app).

## Errors

- **Business outcomes** (not found, refused by a rule, bad or missing argument): a normal result
  `{ "ok": false, "error": { "code": "NOT_FOUND", "message": "No such state.", "field": "stateId" } }`, and
  `{ "ok": true, "data": … }` on success. The app shows a calm outcome card and never counts it as a write.
- **Failures** (the server or its upstream broke): `isError: true` with one plain sentence.
- No stack traces, internal ids or anything sensitive in either.

## Checklist

- [ ] Rules enforced in the API, as the user; no service account
- [ ] A handle per job, set with `chat.set`
- [ ] Short `content`, full `structuredContent`, a named template
- [ ] No secret in any `inputSchema`
- [ ] A read-only preview for every confirm-gated tool
- [ ] `direct` only on small, determined, repeatable tools
- [ ] A completing tool that returns the final status
- [ ] Annotations on every tool
- [ ] 401/403 for auth; an ended session is a 401
- [ ] `{ok, data}` / `{ok:false, error:{code, message, field}}` for business answers
- [ ] `Idempotency-Key` honoured on every write
