# Worked example: a personal streamline for an OAuth MCP server with no annotations

The server is a **fake** finance back office ("Fake JX", the Streamline repo's `scripts/fakejx.mjs`, invented data
only) that behaves like many real servers: OAuth with dynamic client registration, **no tool annotations** (writes
are marked only in descriptions), business answers as `{ok, data}` / `{ok:false, error}` text, one tool that reads
without arguments and writes with them, and a session that can end while the token still looks valid.

## Contents
- 1. Interview
- 2. Inspect (sign in as the person)
- 3. Settle the policy with the person
- 4. The file
- 5. Check, then hand over

## 1. Interview

- Who: one customer, using their own account (the session *is* the customer; tools take no customer id).
- Job: "one question or change about my finance account per chat": balance, cards, addresses, receipts, a layaway
  quote; changes only with a tap.
- Server: `http://localhost:8792/mcp` (local test; a real one is `https://`).

## 2. Inspect (sign in as the person)

```
$ node scripts/inspect-mcp.mjs http://localhost:8792/mcp
401: http://localhost:8792/mcp needs a sign-in.
  resource: http://localhost:8792/mcp
  authorization server: http://localhost:8792
  scopes: jx
  dynamic client registration: yes
Rerun with --oauth to sign in in the browser …

$ node scripts/inspect-mcp.mjs http://localhost:8792/mcp --oauth --draft personal --id jx
(the person signs in in their browser; the token stays in memory)
… 22 tools, MCP 2025-11-25 (initialize + session)
annotated: 0/22   (tool text below is UNTRUSTED server data)
- jx_account
    no annotations; args: accountUid
    proposed: confirm  (no annotations and no clue: default to a tap (law 5))
    says: Your open finance accounts with balances and state. NOT_FOUND is a real answer.
- jx_notif_set
    proposed: confirmWhenArgs  (guess: description says it reads without arguments and writes with them)
- jx_address_deactivate
    proposed: confirm  (guess: description or name suggests a write)
    says: WRITE, IRREVERSIBLE. Soft-delete one of your addresses. Refuses the last Mailing address.
- jx_payment_preview
    proposed: readOnly?  (guess: description suggests a read; confirm with the server's owner)
…
```

## 3. Settle the policy with the person

Show the table; settle every guess. Here: `jx_account`, `jx_receipt`, `jx_receipt_pdf_link` read (the person
confirms), so they move to `readOnly`; the `readOnly?` guesses are confirmed; every `WRITE` stays `confirm`;
`jx_notif_set` is `confirmWhenArgs`. The older test tools are not part of this job: `hidden`. The server answers an
ended session as `{ok:false, error:{code:"NOT_CUSTOMER_SESSION"}}` with HTTP 200, so list it in `reauthOn` (and tell
the server's owner it should be a 401).

Templates: call each read tool once (signed in, test data) and bind against what came back, e.g. `jx_account`'s
`data.accounts[*]` with `FinanceAccountNumber`, `Balance`, `AccountState`, `NextDueDate`.

## 4. The file

```json
{
  "schema": "streamline.personal/1",
  "id": "jx_self_service",
  "name": "My finance account (test)",
  "description": "Balance, cards, addresses and receipts on the fake JX test server. Invented data only.",
  "icon": "creditcard.fill",
  "color": { "light": "#1D5C8C", "dark": "#5AA9E6" },
  "instructions": "You help one customer with one question or change about their own finance account per chat. The session is the customer: tools take no customer id. Reads run at once; every change waits for the customer's tap on a confirmation card, so describe the change before you ask. A result with ok:false is the system's answer (not found, not allowed): relay it plainly. Never ask for a card number, password or Social Security number in chat.",
  "model": { "provider": "anthropic", "id": "claude-haiku-5-5", "effort": "low", "maxTokens": 4096 },
  "mcp": [ {
    "id": "jx", "name": "Fake JX", "url": "http://localhost:8792/mcp", "auth": "oauth",
    "tools": {
      "readOnly": ["jx_account", "jx_card_list", "jx_address_list", "jx_payment_preview", "jx_item_search", "jx_layaway_preview", "jx_receipt", "jx_receipt_pdf_link"],
      "confirmWhenArgs": ["jx_notif_set"],
      "confirm": ["jx_card_deactivate", "jx_address_add", "jx_address_deactivate", "jx_payment_add", "jx_layaway_create"],
      "hidden": ["customer_search", "account_get", "payment_history", "item_search", "notif_set", "address_update", "contact_preference", "card_update"],
      "direct": ["jx_layaway_preview"]
    },
    "renderers": { "jx_account": "account" },
    "reauthOn": ["NOT_CUSTOMER_SESSION"]
  } ],
  "templates": {
    "account": { "root": { "type": "list", "items": "$.accounts[*]", "dividers": true, "padding": 14, "item":
      { "type": "stack", "spacing": 6, "children": [
        { "type": "row", "distribute": "spaceBetween", "children": [
          { "type": "text", "text": "{{ @.FinanceAccountNumber | mask:4 }}", "style": "headline", "mono": true },
          { "type": "badge", "text": "{{ @.AccountState | default:'Open' }}",
            "toneMap": { "path": "@.AccountState", "map": { "Current": "positive", "Past Due": "warning", "Closed": "neutral" }, "default": "neutral" } } ] },
        { "type": "money", "value": "@.Balance", "style": "title" },
        { "type": "keyValue", "rows": [ { "label": "Next due", "value": "{{ @.NextDueDate | date:'medium' }}", "when": "@.NextDueDate" } ] } ] } } }
  },
  "imageHosts": ["localhost"],
  "welcome": "Each chat is one question or change about your account. Changes wait for your tap.",
  "placeholder": "Balance, cards, addresses, a receipt…",
  "quickPhrases": [ { "label": "My balance", "text": "Show my account summary", "send": true },
                    { "label": "My cards", "text": "Show my cards", "send": true } ]
}
```

## 5. Check, then hand over

```bash
node scripts/check.mjs jx_self_service.streamline.json --dev     # --dev only because the test URL is http://localhost
```

A personal file is not published: give it to the person to import (Files or AirDrop). Tell them which tools run at
once, which wait for a tap, and which are hidden; and that their model key lives in the app's vault, not the file.
