# MCP transport and tool guide

## Current status

Machine access is release-gated. Read `GET /v1/capabilities` and require
`machine_access.enabled === true` before using API-key authentication or MCP. When
the flag is false, no public MCP or API-key surface should be assumed from this
static guide.

When enabled, the Streamable HTTP endpoint is `https://app.cleanedweb.com/mcp`. It
uses MCP protocol revision `2025-11-25`, JSON-RPC 2.0, stateless JSON responses, and
workspace-scoped Bearer API keys. The same key can call the HTTP property API.

## Published tool mapping

The gated server publishes exactly these tools through `tools/list`:

| MCP tool | Equivalent HTTP operation | Unit behavior |
| --- | --- | --- |
| `cleanedweb_capabilities` | `GET /v1/capabilities` | No delivery charge |
| `cleanedweb_list_markets` | `GET /v1/markets` | No delivery charge |
| `cleanedweb_count_properties` | `POST /v1/properties/count` | No delivery charge |
| `cleanedweb_search_properties` | `POST /v1/properties/search` | Durable metered receipt |
| `cleanedweb_get_property` | `GET /v1/properties/{property_entity_id}` | No delivery charge |
| `cleanedweb_list_property_changes` | `GET /v1/properties/{property_entity_id}/changes` | No delivery charge |

Every tool includes an `outputSchema` and returns the untouched API result in
`structuredContent`. Capabilities, markets, count, one-property, and property-change tools are
annotated read-only. Search is intentionally not annotated read-only because it commits a
durable Unit receipt; it is annotated non-destructive, idempotent for an exact request,
and closed-domain. All six tools use `openWorldHint: false`. Each output schema accepts
the tool's success object or the stable structured `error` object used when `isError` is
true, so clients can validate both outcomes.

Keep this first tool surface retrieval-only apart from the metered receipt side effect.
Webhook management, account administration,
saved-search mutation, and authentication endpoints should not be exposed as agent
tools without separate approval and confirmation controls.

## Published resources

The server declares resources with `listChanged: false`. `resources/list` publishes:

- live runtime capabilities;
- `property-profile-v2`;
- `property-profile-field-catalog-v2`;
- `property-event-v1`;
- `error-v1`.

These JSON resources are annotated for the assistant audience with explicit priority.
Clients should read capabilities and the property schema first, then the field catalog for
semantics. Resource annotations are hints; schema validation and runtime gates remain the
enforcement boundary.

## Authentication boundary

The MCP client owns a `cw_<environment>_...` Bearer key and must keep it out of
prompts, tool arguments, tool results, URLs, traces, and logs. Keys are scoped to one
workspace, have only `properties:read`, are stored as hashes, and can be revoked.
The full secret is returned only when the key is created.

Every connection is authenticated. If an `Origin` header is present, it must match
`https://app.cleanedweb.com`. The shared per-key request limit returns HTTP `429`
with `Retry-After`; do not defeat it by spreading retries across processes.

## Tool input example

An adapter can accept the same property-search fields defined by OpenAPI:

```json
{
  "market": "LU",
  "response_schema": "property-profile-v2",
  "deal_type": ["buy"],
  "location": "Belair",
  "location_place_id": "lux-neighbourhood-belair",
  "property_categories": ["apartment"],
  "max_price": 1000000,
  "min_bedrooms": 2,
  "limit": 100,
  "max_units": 100,
  "idempotency_key": "search-2026-08-04T10:00:00Z"
}
```

Validate this object against OpenAPI before sending it. Do not add free-form SQL,
arbitrary URLs, acquisition controls, or a generic HTTP passthrough to the tool.

## Tool output policy

Return the API response without renaming contract fields. Preserve:

- `property_entity_id` for canonical identity;
- `schema_version` for validation;
- `snapshot.dataUpdatedAt`, `projectionGenerationId`, `availableRecords`,
  `returned`, `has_more`, and `next_cursor` for acceptance decisions;
- `sources` for provenance.

For conversational use, the adapter may add a compact summary beside the untouched
structured response. It must follow the cursor with the unchanged query when
`has_more` is true, restart after `snapshot_changed`, and not claim freshness
when the runtime timestamp is absent or outside policy.

## Metering example

Call capabilities before a potentially broad search. When machine access is ready,
the runtime publishes the exact delivery rates:

```json
{
  "unit_costs": {
    "first_delivery": 1,
    "unchanged_delivery_within_7_days": 0.1,
    "listing_event_within_7_days": 0.25
  }
}
```

`cleanedweb_search_properties` requires `max_units`, a positive bounded `limit`, and
an `idempotency_key`. Charging, the response receipt, and delivery classification
commit atomically. An exact retry returns the recorded receipt without charging
again. A changed request with the same key returns `idempotency_conflict`.

Use `cleanedweb_count_properties` for exact "how many" questions. It accepts the
search filters but no sort, cursor, limit, spend ceiling, or idempotency key. It
returns only the exact total and projection identity, and consumes no delivery
Units. For "more than two bedrooms", send `min_bedrooms: 3`.

An empty `unit_costs` object still means machine charging is not ready. Do not
estimate a charge or invoke the search tool in that state.

## Error mapping

Map the stable API error object to a tool error without discarding its fields:

```json
{
  "schema_version": "error-v1",
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "details": {}
  }
}
```

The adapter may retry only transient availability failures. It should return
authentication, entitlement, validation, ambiguity, and not-found errors to the
calling agent for a changed plan or user decision.
