MCP server
Endpoint and authentication
- Streamable HTTP at
https://contents.team/api/mcp. - Authenticate with an org API key as a bearer token, exactly as for the REST API. Each tool needs the scopes listed below, so give an assistant's key only what you want it to do. A
ctk_test_key is a safe place to start. - OAuth access tokens (
cto_) work too. A request without a valid credential gets a 401 whoseWWW-Authenticateheader points at/.well-known/oauth-protected-resource/api/mcp, so OAuth-capable clients find the authorization server on their own. - The server is stateless: every call is a plain POST with JSON back, and there are no sessions to keep alive.
- The tools call the same operations as the REST API, with the same scopes, limits, ids, cents and basis points. A test key's
add_photosuses the fixture pipeline; sendX-Contents-Test-Mode: live-aion the connection to use the real one, as on REST. export_reportwaits up to 20 seconds for the file. If it is not ready by then, call it again with the returnedexportId.get_claim_statustakeswaitSeconds(up to 20): while photos are being analyzed it waits for the analysis to finish before answering, so an assistant does not need to poll.- Results are compact JSON, with the same value as
structuredContent. The read tools declare anoutputSchema, published with the tools in mcp.json.get_inventoryreturns 50 items a page unless you send alimit.
Tools
| Tool | What it does | Scopes |
|---|---|---|
list_claims | List the organization's claims, newest first, with item counts and approved RCV in cents. Filter by claimNumber (the number people call a claim by, such as EVAL-2026-0042), externalRef (your own id) or updatedSince; page with cursor. | claims:read |
create_claim | Open a claim with the insured, loss address, loss type and date of loss, and your own externalRef. Returns the claim and its clm_ id. Pass a fresh idempotencyKey for each new claim and reuse it when retrying, so a retry never opens a second claim. | claims:write |
add_photos | Add up to 10 photos to a claim by public https URL, optionally filed by room name. Each photo is analyzed and its items drafted and priced; poll get_claim_status for progress. Pass a fresh idempotencyKey for each batch and reuse it when retrying, so a retry never adds the photos twice. | photos:write |
get_claim_status | How far a claim has got: photos received, queued, analyzing, analyzed and failed; items drafted, awaiting review, approved and rejected; approved RCV, ACV and payment totals in cents. While photos are being analyzed, pass waitSeconds (up to 20) to wait for analysis.status to leave in_progress instead of calling again in quick succession. | claims:read |
get_inventory | The claim's line items: room, description, quantity, condition, status, unit cost in cents, and each line's RCV, depreciation (basis points and cents) and ACV. priced and needsReview say what still needs a person. Filter by status, needsReview, priced or room; 50 items a page by default (limit up to 100), page with cursor. | items:read |
export_report | Generate the XactContents-format XLSX (default), the PDF report or a photo ZIP for a claim. Waits up to 20 seconds and returns the download link when ready; otherwise call again with the returned exportId. Pass a fresh idempotencyKey for each new export and reuse it when retrying, so a retry never starts a second export. | exports:write |
get_replacements | The replacement finder's offers for one item from its latest search, best first: title, retailer, price in cents, link and image, with proposed marking the finder's pick. Each has an rpl_ id for accept_replacement. | items:read |
update_item | Edit one item the way the app does: name, model, quantity, unitCostCents (integer cents; null removes the price), priceSource, priceSourceUrl, roomId (from list_rooms), category (a name or code from list_categories), condition, estimatedAgeYears, notes. Returns the item, revalued. An item still being priced is refused (item_locked). Pass a fresh idempotencyKey for each edit and reuse it when retrying. | items:write |
approve_items | Approve up to 1000 items of a claim, as bulk approval in the app does; only approved items count toward totals and exports. Reports each id as approved, alreadyApproved, locked (still being priced), skipped (with a reason, e.g. unpriced) or notFound. Pass a fresh idempotencyKey for each approval and reuse it when retrying. | items:write |
accept_replacement | Price an item from one of its offers (an rpl_ id from get_replacements) and approve it in one step, as accepting in the app does. The price and quantity come from the offer; if the listing unit count is unclear the call asks you to confirm a quantity (quantity plus quantityConfirmed: true). Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
add_item | Add an item the AI missed, as manual add in the app does: name, optional model, quantity (default 1), unitCostCents (integer cents; omit to add it unpriced), roomId (from list_rooms), condition, estimatedAgeYears, notes. Returns the new item. idempotencyKey is required: send a fresh one per item and reuse it when retrying, so a retry never adds (and bills) a second item. | items:write |
delete_item | Remove one item from the inventory, as delete in the app does. To keep a record that an item was ruled out, use reject_items instead. Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
unapprove_items | Send up to 1000 approved items of a claim back to review, as bulk unapprove in the app does. Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
reject_items | Rule out up to 100 items of a claim (not part of the loss, a duplicate), as rejecting in the app does; rejected items stay on the claim but never count toward totals or exports. Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
list_rooms | The claim's rooms in display order, in one page: floors, rooms and subrooms (parentId), each with its rom_ id and direct item count. Use the rom_ ids as roomId in update_item and add_item, or to filter get_inventory. | claims:read |
get_item | One item by its itm_ id, as get_inventory shows it: room, category, quantity, unit cost in cents, status, and its RCV, depreciation and ACV valued within the claim. | items:read |
list_categories | The categories an item on this claim can take, from the claim's own category system (taxonomy jmidg, or xactimate with 3-letter codes), each with its default depreciation. Pass a name (or, on xactimate claims, a code) as category in update_item. | items:read |
update_claim | Correct the claim record: externalRef, claimNumber, insuredName, lossAddress, lossType, dateOfLoss, insuranceCompany, policyNumber. Send only the fields to change (null clears one); status and valuation settings are not editable here. Returns the claim. Pass a fresh idempotencyKey for each edit and reuse it when retrying. | claims:write |
get_me | The organization, live or test mode, and scopes of the credential this connection uses. Needs no scope: use it to check what the other tools will allow. | none |
get_upload_link | A link that opens the claim in the Contents.team app with its Add photos dialog, for a person to upload photos from their own device. Use it when you cannot send the files yourself (in a chat, say): give the link to the user, who signs in as themselves. The photos are analyzed as usual; follow them with get_claim_status. Not available for test claims, which the app does not show. | claims:read |
create_photo_upload | For clients that can make HTTP PUT requests (Claude Code, scripts): register up to 24 photo files (filename, contentType, exact sizeBytes, optional roomName) and get one presigned PUT URL each. PUT each file's exact bytes to its url with the given Content-Type header within 5 minutes, then call complete_photo_upload with the upl_ id and the pho_ ids you uploaded. If the URLs expired, call this again with a new idempotencyKey. In a chat, use add_photos (public URLs) or get_upload_link instead. | photos:write |
complete_photo_upload | After PUTting the files from create_photo_upload: checks each one arrived with the declared size and type and sends them to analysis, which drafts and prices their items; follow it with get_claim_status. Photos that never arrived are listed in failed. Pass a fresh idempotencyKey and reuse it when retrying. | photos:write |
list_photos | The claim's photos, oldest first: room, kind and analysis status (no download links: to see a photo, use get_photo). Filter by roomId or updatedSince; 25 a page by default (limit up to 100), page with cursor. Use get_photo to look at one. | photos:read |
get_photo | One photo's details plus the image itself, so you can see it: medium (default, up to 1920 px) or thumbnail (400 px). A photo uploaded moments ago may have no image yet; the result then says so. Set includeImage false for the details only. | photos:read |
create_item_photo_upload | For clients that can make HTTP PUT requests: add up to 10 more photos of an item that already exists (a close-up, a receipt, a label) without analyzing them, so no duplicate item is drafted and nothing is billed. Returns one presigned PUT URL per file: PUT the exact bytes with the given Content-Type within 5 minutes, then call attach_item_photos with the pho_ ids. If the URLs expired, call this again with a new idempotencyKey. | photos:writeitems:write |
attach_item_photos | Attach up to 20 photos to an existing item as extra evidence, without analysis: photos uploaded with create_item_photo_upload (checked in storage first), or photos already on the same claim. Returns the item. An item still being priced is refused (item_locked). Pass a fresh idempotencyKey and reuse it when retrying. | photos:writeitems:write |
price_from_link | Price an item from a retailer's product page, as pasting a link in the app does: the page is read and its product name, model, store, link and price become the item's (a page with no price leaves an item that already has a price exactly as it was: its price, store and link stay unchanged). priceMethod search means the page could not be read and the price came from the same store's shopping listing: check it. When priced is false and the item has no price, ask the user for the price and set it with update_item; an item that already had a price keeps it. Set approve true to approve the item too, only when the page itself priced it (a search price is never approved: approveSkipped price_via_search). An item still being priced is refused (item_locked). Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
search_replacements | Run the replacement finder again for one item: a fresh search across retailers, checked against the item's photo. Waits up to about 20 seconds and returns the new offers (status completed); if it is still running (status searching), poll get_replacements until searching is false. Choose an offer with accept_replacement. Approved items are refused (item_already_approved). Searches cost real money, so they are limited to a few a minute per organization: search only items that need it, one at a time. Pass a fresh idempotencyKey and reuse it when retrying. | items:write |
Prompts
The server also offers ready-made prompts. Claude Code lists them as slash commands (/mcp__contents-team__review_claim); other clients show them in their prompt or connector menu. A prompt is only instructions: the tools it calls need their usual scopes, and nothing is approved without asking you.
| Prompt | What it does | Arguments |
|---|---|---|
review_claim | Walk through a claim's draft inventory: fix, price and rule out items, then propose approvals and wait for your go-ahead. | claimId |
price_unpriced_items | Price a claim's unpriced items from the replacement finder's offers, favouring the highest defensible like-kind price. Never approves anything. | claimIdmaxItems (optional) |
add_photos_to_claim | Add photos to a claim (opening one first if needed), wait for the analysis and summarize the drafted items. | claimId (optional)roomName (optional) |
Retries
create_claim, add_photos and export_report take an optional idempotencyKey argument, the MCP form of the REST Idempotency-Key header with the same rules: 1 to 255 visible ASCII characters, kept for 24 hours. Send a new key for each action and the same key when retrying it, and a retry returns the first result instead of making a second claim, batch of photos or export. The same key with different arguments, or on another tool, is a tool error with code idempotency_key_reused; a retry while the first call is still running is idempotency_request_in_progress, so wait a moment and retry. A call that fails frees its key.
Connect a client
Each snippet reads the key from the CONTENTS_API_KEY environment variable. Keep the key out of shared config files and repositories.
Claude Code
claude mcp add --transport http contents-team https://contents.team/api/mcp \
--header "Authorization: Bearer $CONTENTS_API_KEY"Then ask Claude Code to create a test claim called JOB-1042 and show me its inventory. Run claude mcp list to check the connection.
Cursor
{
"mcpServers": {
"contents-team": {
"url": "https://contents.team/api/mcp",
"headers": { "Authorization": "Bearer ${env:CONTENTS_API_KEY}" }
}
}
}Claude Desktop
Claude Desktop starts local servers from its config file, so it reaches a remote server with bearer auth through the mcp-remote bridge. Add this to claude_desktop_config.json and restart the app.
{
"mcpServers": {
"contents-team": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://contents.team/api/mcp",
"--header", "Authorization:Bearer ${CONTENTS_API_KEY}"
],
"env": { "CONTENTS_API_KEY": "ctk_test_..." }
}
}
}Custom connectors on claude.ai and ChatGPT sign in with OAuth rather than a key. They work through OAuth apps, which are in preview; see Authentication.
ChatGPT and the OpenAI API
From your own code, the OpenAI Responses API connects to remote MCP servers and passes the key in a header:
from openai import OpenAI
import os
client = OpenAI()
response = client.responses.create(
model="gpt-5",
tools=[{
"type": "mcp",
"server_label": "contents_team",
"server_url": "https://contents.team/api/mcp",
"headers": {"Authorization": f"Bearer {os.environ['CONTENTS_API_KEY']}"},
"require_approval": "never",
}],
input="List my five newest claims and how many items each has.",
)
print(response.output_text)Good habits
- Give each assistant its own key, so you can see its activity and revoke it alone.
- Leave approval on for the write tools (
create_claim,add_photos,export_report,add_item,update_item,delete_item,approve_items,unapprove_items,reject_items,accept_replacement,update_claim,create_photo_upload,complete_photo_upload,create_item_photo_upload,attach_item_photos,price_from_linkandsearch_replacements) in clients that ask before calling a tool. - An assistant drafts, a person approves. Give a key
items:writeonly when a person signs off on what the assistant approves; without it, items still need a reviewer in Contents.team.