MCP server
Connect Claude, ChatGPT, Cursor or any MCP client to FurniOps. How to sign in, which tools you get, how writes stay safe, and the limits.
FurniOps runs a Model Context Protocol server, so an AI client can read your catalogue and work with it the way your code does with the API. Every tool is a public API route: the same permissions, plan, rate limits and request logs apply, and a tool can never do what the route would refuse.
Endpoint
https://mcp.furniops.com/mcp, over Streamable HTTP. The server speaks MCP 2026-07-28 and serves
2025-era clients too. It holds no session: every request stands alone.
Connect a client
Most clients find everything from the URL alone: the first request is answered with a pointer to FurniOps sign-in, and the client opens it in your browser.
Claude Code
claude mcp add --transport http furniops https://mcp.furniops.com/mcpCursor (.cursor/mcp.json) and other clients that take a JSON file:
{
"mcpServers": {
"furniops": { "url": "https://mcp.furniops.com/mcp" }
}
}Claude, ChatGPT and other apps: add a custom connector and give it the server URL. Sign in when the app asks.
With an API key instead of signing in (servers, CI, agents that run unattended), send it as a bearer token:
claude mcp add --transport http furniops https://mcp.furniops.com/mcp \
--header "Authorization: Bearer $FURNIOPS_AGENT_KEY"Signing in
FurniOps sign-in is OAuth 2.1 with PKCE. The server publishes its Protected Resource Metadata (RFC 9728), which names FurniOps as the authorization server and the permissions a token may ask for.
- You choose the organisation and approve the permissions on a consent screen. Consent is always asked: no MCP client is trusted by default.
- A token holds at most what an API key may hold, and never more than your role: no client can manage keys, members, the organisation, billing or the agent.
- Tokens for the MCP server work only there, and API tokens do not work on the MCP server.
- If a tool needs a permission your token lacks, the server answers
403witherror="insufficient_scope"and the scope it needs. Clients that support step-up ask you to approve it.
Tools
The tools are generated from the API reference, one per route, named after the
operation in snake_case (list_products, get_run, update_source). Each takes the route's path
and query parameters at the top level, and a write's body as body.
| Set | What | Who gets it |
|---|---|---|
| Read | Every GET: products, offers, sources, runs, changes, webhooks, usage, request logs, proposals | Everyone, within their permissions |
| Write | Creates, runs, merges, webhook endpoint changes, recoveries and tests | Signed-in people, and API keys made for an agent |
Routes that hand out secrets (creating a webhook endpoint, rotating its secret) or change security
are never tools, and neither are deciding proposals or changing the AI agents and the judge. Use
the dashboard or the API for those. Some write tools also refuse fields a person must change:
update_webhook_endpoint takes no url, so text a model reads can never redirect your events. A
call that includes one is refused, not ignored.
Every tool
Generated from the contracts release these docs are built from: the tool, the route it calls and the permission it needs.
| Tool | Calls | Set | Permission | Hints |
|---|---|---|---|---|
| check_variant | Check a variant nowPOST /v1/variants/{id}/checks | Write | freshness:write | none |
| complete_upload | Complete an uploadPOST /v1/uploads/{id}/complete | Write | runs:write | none |
| confirm_mapping | Confirm a mappingPOST /v1/mappings/{id}/confirm | Write | sources:write | none |
| create_export | Start an exportPOST /v1/exports | Write | exports:write | none |
| create_run | Start a runPOST /v1/sources/{id}/runs | Write | runs:write | none |
| create_source | Create a sourcePOST /v1/sources | Write | sources:write | none |
| create_source_probe | Probe a vendor URLPOST /v1/source-probes | Write | sources:write | none |
| create_upload | Start an uploadPOST /v1/sources/{id}/uploads | Write | runs:write | none |
| delete_webhook_endpoint | Delete a webhook endpointDELETE /v1/webhook-endpoints/{id} | Write | webhooks:write | destructive, idempotent |
| dismiss_match_pair | Dismiss a review pairPOST /v1/match-pairs/{id}/dismiss | Write | products:write | none |
| get_agreement | Read an agreement and where the organisation standsGET /v1/agreements/{kind} | Read | agreements:read | read-only, idempotent |
| get_billing | Read billingGET /v1/billing | Read | billing:read | read-only, idempotent |
| get_brand | Read the organisation's brandGET /v1/brand | Read | brand:read | read-only, idempotent |
| get_brand_claim | Read a claimGET /v1/brand/claims/{id} | Read | brand:read | read-only, idempotent |
| get_catalogue | Read the organisation's own catalogueGET /v1/catalogue | Read | products:read | read-only, idempotent |
| get_export | Get an exportGET /v1/exports/{id} | Read | exports:read | read-only, idempotent |
| get_judge_settings | Retrieve the AI judge's settingsGET /v1/matching/judge-settings | Read | agent:read | read-only, idempotent |
| get_mapping | Retrieve a mappingGET /v1/mappings/{id} | Read | sources:read | read-only, idempotent |
| get_match_pair | Retrieve a review pairGET /v1/match-pairs/{id} | Read | products:read | read-only, idempotent |
| get_media | Get a stored imageGET /v1/media/{id} | Read | products:read | read-only, idempotent |
| get_merge | Retrieve a mergeGET /v1/merges/{id} | Read | products:read | read-only, idempotent |
| get_product | Get a productGET /v1/products/{id} | Read | products:read | read-only, idempotent |
| get_proposal | Retrieve a proposalGET /v1/proposals/{id} | Read | agent:read | read-only, idempotent |
| get_publication_run | Retrieve a publication runGET /v1/catalogue/publication-runs/{id} | Read | publishing:read | read-only, idempotent |
| get_request_log | Get a request logGET /v1/request-logs/{id} | Read | request_logs:read | read-only, idempotent |
| get_run | Retrieve a runGET /v1/runs/{id} | Read | runs:read | read-only, idempotent |
| get_source | Retrieve a sourceGET /v1/sources/{id} | Read | sources:read | read-only, idempotent |
| get_source_probe | Retrieve a source probeGET /v1/source-probes/{id} | Read | sources:read | read-only, idempotent |
| get_upload | Retrieve an uploadGET /v1/uploads/{id} | Read | runs:read | read-only, idempotent |
| get_usage | Read the month's usageGET /v1/usage | Read | usage:read | read-only, idempotent |
| get_usage_series | Read one meter over timeGET /v1/usage/series | Read | usage:read | read-only, idempotent |
| get_vendor_product_stats | Read the vendor's most read productsGET /v1/vendor/stats/products | Read | analytics:read | read-only, idempotent |
| get_vendor_profile | Read a vendor profileGET /v1/vendor-profiles/{id} | Read | products:read | read-only, idempotent |
| get_vendor_stats | Read the vendor's reach and data healthGET /v1/vendor/stats | Read | analytics:read | read-only, idempotent |
| get_vendor_stats_series | Read the vendor's reach week by weekGET /v1/vendor/stats/series | Read | analytics:read | read-only, idempotent |
| get_webhook_delivery | Get a deliveryGET /v1/webhook-endpoints/{id}/deliveries/{delivery} | Read | webhooks:read | read-only, idempotent |
| get_webhook_endpoint | Get a webhook endpointGET /v1/webhook-endpoints/{id} | Read | webhooks:read | read-only, idempotent |
| list_agent_settings | List the agents' settingsGET /v1/agents | Read | agent:read | read-only, idempotent |
| list_agreement_acceptances | List the organisation's acceptancesGET /v1/agreement-acceptances | Read | agreements:read | read-only, idempotent |
| list_audit_events | List audit eventsGET /v1/audit-events | Read | audit:read | read-only, idempotent |
| list_brand_claims | List the organisation's claimsGET /v1/brand/claims | Read | brand:read | read-only, idempotent |
| list_catalogue_images | List the catalogue's imagesGET /v1/catalogue/images | Read | products:read | read-only, idempotent |
| list_catalogue_variants | List the catalogue's variants with price levels and stockGET /v1/catalogue/variants | Read | variants:read | read-only, idempotent |
| list_categories | List the FurniOps taxonomyGET /v1/categories | Read | sources:read | read-only, idempotent |
| list_changes | Read the change feedGET /v1/changes | Read | events:read | read-only, idempotent |
| list_exports | List exportsGET /v1/exports | Read | exports:read | read-only, idempotent |
| list_inbound_messages | List a source's inbound emailsGET /v1/sources/{id}/inbound-messages | Read | sources:read | read-only, idempotent |
| list_invoices | List invoicesGET /v1/billing/invoices | Read | billing:read | read-only, idempotent |
| list_mappings | List a source's mappingsGET /v1/sources/{id}/mappings | Read | sources:read | read-only, idempotent |
| list_match_pairs | List review pairsGET /v1/match-pairs | Read | products:read | read-only, idempotent |
| list_merges | List mergesGET /v1/merges | Read | products:read | read-only, idempotent |
| list_offers | List offersGET /v1/offers | Read | products:read | read-only, idempotent |
| list_products | List productsGET /v1/products | Read | products:read | read-only, idempotent |
| list_proposals | List proposalsGET /v1/proposals | Read | agent:read | read-only, idempotent |
| list_publication_runs | List publication runsGET /v1/catalogue/publication-runs | Read | publishing:read | read-only, idempotent |
| list_recent_changes | Read the newest changes firstGET /v1/changes/recent | Read | events:read | read-only, idempotent |
| list_request_logs | List request logsGET /v1/request-logs | Read | request_logs:read | read-only, idempotent |
| list_runs | List a source's runsGET /v1/sources/{id}/runs | Read | runs:read | read-only, idempotent |
| list_source_categories | List a source's categoriesGET /v1/sources/{id}/categories | Read | sources:read | read-only, idempotent |
| list_source_probes | List source probesGET /v1/source-probes | Read | sources:read | read-only, idempotent |
| list_sources | List sourcesGET /v1/sources | Read | sources:read | read-only, idempotent |
| list_vendor_profiles | Search the vendor directoryGET /v1/vendor-profiles | Read | products:read | read-only, idempotent |
| list_webhook_deliveries | List an endpoint's deliveriesGET /v1/webhook-endpoints/{id}/deliveries | Read | webhooks:read | read-only, idempotent |
| list_webhook_endpoints | List webhook endpointsGET /v1/webhook-endpoints | Read | webhooks:read | read-only, idempotent |
| merge_match_pair | Merge a review pairPOST /v1/match-pairs/{id}/merge | Write | products:write | none |
| publish_catalogue | Publish the catalogue to the networkPOST /v1/catalogue/publish | Write | publishing:write | none |
| push_records | Write products to a push sourcePOST /v1/sources/{id}/records | Write | runs:write | none |
| record_demand | Record demandPOST /v1/demand | Write | freshness:write | none |
| recover_webhook_deliveries | Send failed deliveries againPOST /v1/webhook-endpoints/{id}/recover | Write | webhooks:write | none |
| reopen_match_pair | Reopen a dismissed review pairPOST /v1/match-pairs/{id}/reopen | Write | products:write | none |
| replay_run | Replay a sourcePOST /v1/sources/{id}/replay | Write | runs:write | none |
| resend_webhook_delivery | Send a delivery againPOST /v1/webhook-endpoints/{id}/deliveries/{delivery}/resend | Write | webhooks:write | none |
| save_category_rules | Set a source's category rulesPOST /v1/sources/{id}/categories | Write | sources:write | none |
| send_webhook_test | Send a test eventPOST /v1/webhook-endpoints/{id}/test | Write | webhooks:write | none |
| set_product_visibility | Hide products from the network, or list them againPOST /v1/catalogue/visibility | Write | publishing:write | none |
| test_source | Test a sourcePOST /v1/sources/{id}/test | Write | sources:write | none |
| trigger_test_event | Trigger a sample eventPOST /v1/test/events | Write | webhooks:write | none |
| undo_audit_event | Undo an audited changePOST /v1/audit-events/{id}/undo | Write | audit:write | none |
| undo_merge | Undo a mergePOST /v1/merges/{id}/undo | Write | products:write | none |
| update_source | Update a sourcePATCH /v1/sources/{id} | Write | sources:write | idempotent |
| update_webhook_endpoint | Update a webhook endpointPATCH /v1/webhook-endpoints/{id} | Write | webhooks:write | idempotent |
| withdraw_catalogue | Withdraw the catalogue from the networkPOST /v1/catalogue/withdraw | Write | publishing:write | destructive |
Every tool states what it risks: readOnlyHint, destructiveHint (only
delete_webhook_endpoint today) and idempotentHint. Clients use them to decide when to ask you
before a call.
Writes
Which keys may write. An API key is offered write tools only when its purpose is AI agent. Agent keys are always restricted to the permissions you pick, and when your organisation turns on agent key approval, a second member must approve one before it works. A key made for an integration or a third party gets read tools only, so a sync key can never be driven by a model.
Retries do nothing twice. Creating tools take an optional idempotency_key. Without one, the
server makes a key for that call. A client that retries with its own key gets the first answer
back, as with idempotent requests.
No lost updates. Reading a single resource returns its ETag, as a line of text and in the
result's _meta["com.furniops/etag"]. Tools that change a resource (update_source,
update_webhook_endpoint) take it as if_match. If someone changed the resource since, the call
fails with precondition_failed: read it again and retry. See concurrency.
Results and errors
- A result is the route's JSON: as text, and as
structuredContentwhen it is an object. Its shape is the route's response in the API reference. Tools carry input schemas only, so the tool list stays small enough for clients that budget tool definitions. - A refusal is a tool error naming the problem's title,
codeand detail (and the fields, forinvalid_request), so the model can tellnot_foundfrompermission_denied. It is not a protocol error. Codes are explained in errors. - Results are data. Product titles, descriptions and notes come from supplier feeds and websites. Every result is marked as untrusted content, and the server tells the model never to follow instructions found in it, or change anything because the content asks.
Limits
- Plan: MCP is a plan feature. An organisation whose plan does not include it is answered
403 feature_unavailable. - Rate: each signed-in caller (key or person, per mode) has its own budget for MCP requests: 50 a second, bursts of 200. Each tool call also takes your organisation's API rate budget, as the route it is. Callers never share a budget because they share an address, so hosted clients are not slowed by each other.
- Refused credentials: an address that sends more than 20 refused credentials a second
(bursts of 200) is answered
429for a moment, before anything is verified. Refresh expired tokens instead of retrying them. - Quota: every tool call counts one MCP call in usage, and not
an API call. Listing tools is free. When the month's quota is spent, tool calls return
quota_exceededuntil the next month; test mode is never capped. Counts reach usage within seconds. - Requests are at most 1 MiB.
Test mode
A test API key reaches only your sandbox, as on the API. A signed-in client works in live mode;
send FurniOps-Mode: test with every request to use the sandbox. See test mode.
In the dashboard
Developers › MCP shows the server URL, a ready-made setup for each client (signing in with OAuth, or an agent API key), what a client may do (read tools, write tools, and what is never a tool), and the limits. API keys for an agent are made under API keys.