# fixRAgent remote MCP server

**Endpoint: `https://fixragent.com/mcp`** — Streamable HTTP, no install, no download.

This is the hosted server. It is not the same thing as the `.mcpb` bundle in this folder: that one
runs on your own machine and reads photos off your disk; this one runs on ours and takes the photo
in the call. Use whichever fits. The tools are named for the job, not for our product, and both
servers end up at the same engine.

**Stated exactly (BLOCK-442):** this is the remote, keyed, Streamable-HTTP shape that ChatGPT's
directory and Anthropic's custom connectors require, and it has been driven end to end by the
reference MCP Inspector. It is **not listed in ChatGPT's directory** — nobody has submitted it — so
"works in ChatGPT" is a claim this file does not make and has never measured.

| | Remote (this file) | Local bundle (`README.md`) |
|---|---|---|
| Where it runs | fixragent.com | your machine |
| Install | paste a URL | download a `.mcpb` |
| Photo from a local path | no — send `image_base64` | yes — `image_path` |
| Shape a directory or a phone client can take | yes | no |

## The three tools

**`try_sample`** (BLOCK-603) — no key, no input. It returns one stored result: the real engine's
captured answer for a sample photo from fixRAgent's test set (an HVAC air handler and furnace, 13 Sep 2026),
labelled `SAMPLE`, plus how to get a free key. It reads no photo, calls no model, writes nothing and
costs $0; it is bounded at 30 calls an hour per address, in the instance's memory. The photo itself
is described, not served (it is held under a processing-only licence). `readOnlyHint: true`,
`destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false`. `assess_property_photo`
sent with no key returns the same stored result in its own shape (`sample: true`, warnings[0] starts `SAMPLE:`, no
`outcome_url`) and does not read the photo. The answer is copied from
`qa/api-launch-kit/captured/` into `api/_mcp-sample.json` by `qa/mcp/build-sample.js`, and
`qa/mcp/remote-check.js` fails if the two disagree.

`Mcp-Name` (2026-07-28) carries the tool name on `tools/call` and is checked against `params.name`
after Base64-sentinel decoding; it does not name the server.

**`assess_property_photo`** — one photo of something in a building in, the fixed triage out: what the
asset is, whether a fault is visible, one of four urgency words (EMERGENCY, TODAY, THIS WEEK,
WHENEVER), which trade to call, a line to say to the resident, how many independent reads agreed, and
a `share_url` for the Triage Profile. Writes one assessment and spends one of the key's daily
requests. `readOnlyHint: false`, `destructiveHint: false`, `openWorldHint: true`.

**`get_triage_profile`** — a `diagnosis_id` (or a pasted `fixragent.com/c/<id>` link) in, the saved
assessment out. It calls no model and spends nothing from the key's daily allowance; it changes
nothing and returns no contact details. `readOnlyHint: true`, `destructiveHint: false`,
`openWorldHint: true`.

Said precisely, because an earlier draft of this file said "one read": for a triage id it is **two
SELECTs**, not one — `api/shared-diagnosis.js` looks in `diagnoses` first and, finding nothing there
(a triage id never lives in that table), looks in `diy_guest_scans`. Both are indexed reads of one
row by id. It still calls no model and still costs nobody a request from their daily allowance.

It is not a medical, veterinary or automotive tool, and it is not for an emergency in progress. A
fire, a gas smell, a sounding carbon-monoxide alarm, water on live electrics or anyone hurt is a 911
call (or your local emergency number), not a tool call — the tool descriptions say so, so a model
that reads them routes those away from us.

## The key

A free demo key is issued at <https://fixragent.com/docs#key>. Send it any of three ways, because
different clients give you different boxes to type in:

| How | When to use it |
|---|---|
| `x-triage-key: <key>` header | a client with a custom-headers field (Claude Code, Cursor, the Inspector) |
| `Authorization: Bearer <key>` | a client that only offers a bearer token |
| `https://fixragent.com/mcp?k=<key>` | a client whose whole configuration is one URL (Claude's "add custom connector") — **see the caveat below** |

**Prefer a header.** A key in `?k=` is written into our hosting platform's request log, because
Vercel's runtime logs retain **Search Params** as a log field (1 day on Pro, 30 days with
Observability Plus). No line of our code logs it — the platform does, before our code runs. The two
header doors avoid that entirely, so use one wherever your client offers it. `?k=` exists only
because some clients (Claude's "add custom connector" box) accept nothing but a URL; if you use it,
treat that key as one you can rotate at <https://fixragent.com/docs#key> and not as a long-lived
secret.

Discovery needs no key: `initialize`, `tools/list`, `ping` and `server/discover` answer to anyone, so
a directory's scanner can read the tool metadata without one and without costing anybody a model
call. Only `tools/call` needs a key, and not for `try_sample` (or `assess_property_photo` sent with no key, which returns the same SAMPLE).

**OAuth 2.0 is not implemented.** It is the named next step if a directory requires it for listing; the
key door is what ships today. Nothing here pretends otherwise.

## Adding it

**Claude (Pro/Max):** Customize → Connectors → "+" → Add custom connector → paste
`https://fixragent.com/mcp?k=<your key>`.
**Claude (Team/Enterprise):** an Owner adds it under Organization settings → Connectors → Add →
Custom → Web, then each member connects.
(Steps read from support.claude.com article 11175166 on 2026-09-23.)

**Claude Code:**
```bash
claude mcp add --transport http fixragent https://fixragent.com/mcp --header "x-triage-key: <your key>"
```

**MCP Inspector:**
```bash
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP · URL: https://fixragent.com/mcp · Header: x-triage-key
```

**Anything else:** POST JSON-RPC to the URL. `curl` is a complete client:
```bash
curl -s https://fixragent.com/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## What it speaks

Both MCP eras from the one endpoint:

* **2025-11-25 / 2025-06-18** — the `initialize` handshake every shipping client uses today. A
  client that asks for an older revision is negotiated down to one of these rather than refused.
  2025-03-26 and 2024-11-05 are deliberately **not** advertised here: both permit JSON-RPC batches,
  which this endpoint refuses, and 2024-11-05's HTTP binding is HTTP+SSE, which this endpoint does
  not serve. The `.mcpb` bundle, which is stdio, still speaks all four.
* **2026-07-28** — per-request `_meta`, `server/discover`, `resultType`.

It holds **no session**: it mints no `Mcp-Session-Id`, ignores one if you send it, ignores
`Last-Event-ID`, and answers `GET` and `DELETE` with `405` — which is what the 2026-07-28 revision
tells a server to do, and a legal answer for a 2025-era server that offers no standalone SSE stream.
Replies are a single `application/json` object; nothing here streams, so an SSE stream would be a
costume. JSON-RPC batching is refused (it left MCP in 2025-06-18).

`Origin` is validated on every request. No `Origin` (any ordinary MCP client) passes;
`https://fixragent.com` and loopback (the Inspector) pass; any other browser origin gets `403`. That
is the spec's DNS-rebinding rule, and it is also why a stranger's web page cannot spend our engine
budget through a visitor's browser.

## Limits, and what a call costs you

* **3 MB** per photo once decoded, at least 1 KB. JPEG, PNG or WebP.
* **60 assessments a day** on a demo key; **20 requests an hour** per address; a daily compute fuse on
  our side. Every refusal comes back as a sentence naming which limit it was and when it resets —
  "no key" and "wrong key" never read the same.
* `get_triage_profile` counts against **neither the 60-a-day key allowance nor the compute fuse** —
  it calls no model. It does count against the **20 requests an hour per address**, which is the
  bound on that path; before BLOCK-442 that path had no bound at all. Stated at its real scope:
  that counter lives in the serving instance's memory, so it bounds a caller per warm instance.
  It is the same bound `api/triage.js` has, no better and no worse.
* An assessment runs three reads on `fast` and five on `deep`, so it is not instant. The function's
  ceiling is 60 seconds; set a client timeout to match.

## What we do with the photo

The photo's location and camera metadata (EXIF/XMP/IPTC) is removed **on our server**, before the
photo is hashed, read by the model, or stored. Said the uncomfortable way round: it is **not** removed
before it leaves your machine — you are sending us the file you have. The assessment is stored with a
SHA-256 of the *stripped* photo; the photo itself is not kept. No contact details are asked for and
none are returned. Full text: <https://fixragent.com/privacy>.

Every call is tagged with the app it came from (`channel: mcp`, `variant: source:mcp-<client>`), which
is how we know which AI app people are reaching us through. It carries no user identity.

## One engine, not two

`api/mcp.js` calls `api/triage.js` and `api/shared-diagnosis.js` in-process. There is no second
engine, no copied prompt and no parallel contract: whatever the live triage path does — the EXIF
strip, the key door, the per-IP limit, the spend fuse, the contract enforcement — this endpoint does,
on the same commit, because it *is* that path. The check is `qa/mcp/remote-check.js`.

Made by AR Logic LLC. Terms: <https://fixragent.com/terms> · Support: <https://fixragent.com/docs>
