Reviewing a server; what to ask its owner to change
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
- 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.
- One job, one handle. The first tool that creates the job's object returns its id (
cartId,poId) withchat.setso the stream keeps it; later tools take it as an argument. MCP 2026-07-28 is stateless. - 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. - **No secret in any
inputSchema.** SSN, date of birth, license, PIN, password, income for a credit check: return a sealed form (MCP form-mode elicitation naming a configured form, or thesealedFormdirective). Cards not present: URL-mode elicitation to the processor's hosted page. Cards present: a terminal tool. - 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. - **
directtools** take fully determined arguments, do one small thing, are safe to run twice. - Completion: the completing tool returns the final identifiers and
complete: true(or is listed incompletion.tools);chat.status: "waiting"while blocked on someone else; never report success not achieved. - 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 inreauthOnas a stopgap). - Act as the person, never a service account; allow calls from phones (no IP-pinning to Streamliner).
- Scans and files are plain arguments. A scan arrives as a string (
code,sku): accept GS1 element strings, validate in the API, answer an unknown code as a business outcome. An attachment reaches a tool only as an upload reference the app swaps for a 15-minute signed link (attachments.toModel: false); fetch it once, never store it. Personal streamlines can take the file in the call: mark the property"type": "string", "contentEncoding": "base64", "contentMediaType": "text/csv"; the model passes theatt_…id, and after the gates the app sends the base64 of the file as picked (text files unredacted, images as processed), the model keeping only the id. At most about 6 MB of files per call. List such tools only to a client whose capabilities include"com.manolab.streamline/files": {}; keep another way in for every other client. - Idempotency: honor
Idempotency-Keyon every write; the same key returns the same result with no second effect.
Result shape
{ "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: truewith 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, fullstructuredContent, a named template - [ ] No secret in any
inputSchema - [ ] A read-only preview for every confirm-gated tool
- [ ]
directonly 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-Keyhonored on every write
Narrow per person, and name tools by area (2026-10-12)
- **Answer
tools/listper token** with only what this person may call, and sendnotifications/tools/list_changedwhen that changes. The app lists tools with the person's own credential, so they never see the rest. - If you list everything, refuse a call the person may not make clearly: HTTP 403 (without
insufficient_scope, which means "sign in again"), or{ "ok": false, "error": { "code": "FORBIDDEN", "message": "…" } }. The app then stops offering that tool to that person for a day. Never use FORBIDDEN for a rule about one record (another store's offer): give that its own code, or the tool vanishes for someone who may use it. - Name tools by area (
customer_search,customer_create,inventory_item_get) so a streamline can admit an area with a pattern (customer_*) and new tools land where they belong.