UCP catalog
Search every Fabric brand's approved products from a shopping agent.
Fabric's UCP catalog is one Universal Commerce Protocol endpoint that answers catalog searches across every brand Fabric serves. Each product names the brand that sells it and links to that brand's store, where the shopper buys.
Note
The catalog answers on https://app.staging.fabric.inc while it is in staging. The paths below
stay the same when it moves to production.
Discovery
The business profile is at /.well-known/ucp. It needs no credentials and is cached for 5 minutes.
curl https://app.staging.fabric.inc/.well-known/ucpIt declares UCP 2026-08-25 (with 2026-04-08 under supported_versions), the
dev.ucp.shopping.catalog.search and dev.ucp.shopping.catalog.lookup capabilities, and one
dev.ucp.shopping service over mcp whose endpoint is /api/ucp/mcp. It declares no cart,
checkout or order capability, and no payment handler.
Identifying your agent
There is no API key. Every tool call carries your agent's UCP profile URL in
meta["ucp-agent"].profile, as it does with any UCP business:
- The URL must be
httpson the default port, publicly reachable, and must not redirect. Its query string is ignored. - The profile must declare a UCP
versionand at least one catalog capability. - Fabric fetches it once and reuses it for 10 minutes.
Calling the catalog
The endpoint speaks MCP over streamable HTTP, statelessly: every request is a single POST with
Accept: application/json, text/event-stream. Batches are refused.
| Tool | Does |
|---|---|
search_catalog | Searches all brands, or the ones in catalog.filters.business |
lookup_catalog | Returns up to 100 products by id |
get_product | Returns one product by id |
curl -X POST https://app.staging.fabric.inc/api/ucp/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_catalog",
"arguments": {
"meta": { "ucp-agent": { "profile": "https://agent.example.com/.well-known/ucp" } },
"catalog": {
"query": "waterproof hiking boots",
"filters": { "business": ["alpenglowsports.com"] },
"pagination": { "limit": 10 }
}
}
}
}'The UCP result is in result.structuredContent, and the same JSON is in result.content[0].text.
catalog.queryis at most 256 characters, and each filter value at most 128.catalog.filters.businesstakes one brand domain or a list of up to 20. Leave it out to search every brand.pagination.limitdefaults to 10 and is capped at 100. A search with aqueryreturns the best matches in one page; a search without one pages withpagination.cursor.- Price filters (
catalog.filters.price.minand.max) are in minor units of each product's own currency.
What a product carries
idnames the brand and the SKU, for examplegid://fabric/Product/alpenglowsports.com/BOOT-7. Pass it back tolookup_catalogorget_productunchanged.urlis the product page on the brand's store. That is where the shopper buys.variants[].sellerholds the brand's name and a link to its store, andmetadata.business_domainholds its domain.variants[].priceis the brand's price from its last catalog import, in the brand's currency.variants[].availabilityis always{ "available": true, "status": "in_stock" }. Fabric does not hold live stock, so check the brand's store before promising delivery.
Only products each brand has approved for publishing are served. A product without a price is left out.
Errors
A refused caller gets a JSON-RPC error with code -32001 and the reason in data.code:
data.code | HTTP | Meaning |
|---|---|---|
invalid_profile_url | 200 | No profile URL in meta, or it is not https on the default port |
profile_unreachable | 422 | The profile could not be fetched, or Fabric has fetched too many profiles for now (the message says which) |
invalid_profile | 200 | The profile is not a UCP profile, or declares no catalog capability |
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "A UCP agent profile is required: send meta[\"ucp-agent\"].profile.",
"data": { "code": "invalid_profile_url" }
}
}A tool that does not exist, such as create_cart, returns a tool result with isError: true. So
does a malformed call; its text starts with the JSON-RPC code, for example -32602: ....
A request body over 64 KB returns 413. When the catalog is busy, a call returns 503 with
Retry-After: 1.
Rate limits
| Counted per | Limit |
|---|---|
| All callers together | 1,200 requests a minute, and at most 40 a second on any one server |
| Client IP | 30 requests a minute, 600 an hour |
| Agent profile host | 300 tool calls a minute, 10,000 an hour |
| Profile fetches, per profile URL | 20 an hour |
| Profile fetches, all callers together | 60 a minute |
A profile Fabric has already fetched successfully is not held to the fetch limits.
Every successful response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
Over a limit, the endpoint returns 429 with Retry-After in seconds. The MCP handshake
(initialize, tools/list) counts toward the first two.
Note
Your agent is identified by the profile URL it sends; requests are not signed yet. Its budget is counted per profile host, and shared by everyone who presents a profile on that host.