Looks MCP Server
by montebelle
README.md
# @thelooks/sdk
**Official TypeScript SDK for the Look API — outfit generation for a real venue, plus an analytics surface built around what it refuses to answer.** Ships the client, the Model Context Protocol tool definitions, and the OpenAPI document, all generated from the server so none of them can drift from it.
[](https://www.npmjs.com/package/@thelooks/sdk)
[](LICENSE)
[](https://nodejs.org/)
[](package.json)
[](#mcp)
```bash
npm install @thelooks/sdk
```
```ts
import { LookClient } from '@thelooks/sdk';
const look = new LookClient({ apiKey: process.env.LOOK_API_KEY! });
// Generation takes 50–80s, so it is async by design: enqueue, then wait.
const { jobId } = await look.looks.create({
message: 'dinner then drinks',
department: 'mens',
place: { name: 'Balthazar', place_id: 'ChIJ...' },
});
const result = await look.looks.wait(jobId);
```
Look **starts with where**. A `place` carrying a `name`, or a `generalLocation`, is required — the client throws before making a request if neither is present. Every call needs `Authorization: Bearer sk_live_…`; keys are minted per organisation and carry scopes, so a key reaches only the endpoints its scopes allow.
## Generation
- **Async by default** — `looks.create()` returns a job handle immediately and `looks.wait()` polls to completion. Generation is 50–80s, so a synchronous contract would have been a lie
- **`looks.stream()`** — server-sent events for progress, instead of polling
- **`looks.refine()`** — adjust a finished look without regenerating it from scratch
- **`looks.describe()` / `looks.image()`** — natural-language description, and a rendered image of the look
- **Idempotency** — pass `idempotencyKey` so a retried enqueue cannot double-charge
- **End-user attribution** — `X-End-User-Id` scopes a call to one of *your* users without minting them a key
## Analytics
Behind its own scope, `analytics:read`, deliberately not granted by `looks:read`. Reporting is treated as more sensitive than transactional access, not less.
```ts
const { data, meta } = await look.analytics.items({
from: '2026-06-01',
to: '2026-07-01', // EXCLUSIVE — the range is half-open [from, to)
granularity: 'day',
groupBy: ['brand'], // at most two dimensions
});
```
**Read `meta` before you read `data`.** Three independent flags; conflating them produces confidently wrong numbers:
- **`truncated`** — more rows exist, and paging with `offset` **will** reach them
- **`suppressed`** — cells were withheld under a k-anonymity floor, and paging will **never** reach them
- **`sampled`** — the underlying read was capped, so every figure is a **floor**, not a total
A withheld cell is **absent from the results — never a row with zero.** Do not report a missing row as "no activity"; report it as withheld.
Two more things worth knowing before building a dashboard:
- **`surfaced` and `shown` differ by roughly 10×** — a generation stores ~40 SERP candidates and shows four. Use `shown` as the exposure denominator, or every rate lands an order of magnitude too low
- **Place figures are declared intent** — someone told us where they were going. Not foot traffic, not visits, not inferred presence. Please don't describe them as visitation
Check a query shape before spending budget on it. `compatibility` runs nothing and names the limit that would refuse it:
```ts
const check = await look.analytics.compatibility({
endpoint: 'places', from: '2026-06-01', to: '2026-07-01', granularity: 'hour',
});
// { compatible: false, reason: 'A `hour` query may span at most 7 days.', … }
```
## Bulk export
```ts
const { exportId } = await look.analytics.createExport({
endpoint: 'items', from: '2026-06-01', to: '2026-07-01', format: 'csv',
});
const done = await look.analytics.getExport(exportId); // → { url, metaUrl }
```
Requires `analytics:export`. Exports run the **same** queries as the sync endpoints, so suppression and venue exclusion are not bypassed. JSON carries `meta` inside the payload; CSV has nowhere to put an envelope, so the flags arrive as a sidecar at `metaUrl` — without it, a CSV of suppressed demand is indistinguishable from a CSV of zero demand.
## MCP
The analytics surface is also exposed as [Model Context Protocol](https://modelcontextprotocol.io) tools at `POST /api/v1/mcp`, on the same credential and scopes. The definitions ship here so an agent host can register them without a round trip:
```ts
import { ANALYTICS_TOOLS, findTool } from '@thelooks/sdk/mcp';
```
They are generated from the same source the server answers `tools/list` from, so they cannot drift from what the endpoint accepts.
**Pass the descriptions through intact.** They carry semantics no JSON schema can express — the `surfaced`/`shown` ratio, that a withheld cell is absent rather than zero, that place demand is declared intent. An agent that sees only the schema will state those wrongly, and state them fluently.
## OpenAPI
```ts
import spec from '@thelooks/sdk/openapi.json' with { type: 'json' };
```
37 paths, generated from the same zod schemas the routes validate with, so the document cannot describe an API that does not exist.
## Errors
```ts
import { LookApiError } from '@thelooks/sdk';
try {
await look.looks.create({ /* … */ });
} catch (err) {
if (err instanceof LookApiError) {
// isClient is true for 4xx — bad request, auth, scope. Retrying will not help.
console.error(err.status, err.type, err.message, err.isClient);
}
}
```
## API surface
| Namespace | What it covers |
|---|---|
| `looks` | `create`, `wait`, `get`, `refine`, `stream`, `describe`, `image` |
| `analytics` | `looks`, `items`, `places`, `users`, `compatibility`, `createExport`, `getExport` |
| `shop` | search and browse |
| `closet`, `savedItems`, `savedLooks`, `photos`, `cart` | everything a user owns, saved, or is considering |
| `items` | the garments a generation produced |
| `places`, `locations`, `sessions` | venue and session context |
| `preferences` | body, sizes, fit, style |
| `keys` | mint, list and revoke API keys |
## Licence
MIT. This licence covers the SDK; use of the Look API itself is governed by its terms of service.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues