---
name: set-api-connection
description: How to connect to the Set World API. Use before the first call to check the service, discover endpoints from /api/spec, read the error envelope and status codes, pass seeds, keep contentVersion for replay, request tables as CSV, or connect an agent over MCP. Includes the schemas of the spec document, the error envelope and the MCP tools.
license: Free to use when obtained from set.world, commercially too (the API, what it returns, the skills and downloaded models). The source code of Set is © Jimmy Lee under CC BY-NC 4.0 and its commercial use needs a paid waiver.
metadata:
  version: "0.8.9"
  title: API connection
---

# API connection

Set is a stateless JSON API at `https://set.world`. There is no key, account, SDK or rate-limit header. Every call is a `GET` with query parameters, except the MCP endpoint, which takes `POST`. This skill owns the conventions every other skill relies on.

## Check the connection

```sh
curl https://set.world/api
```

```json
{ "hello": "set world" }
```

It returns this greeting and nothing else.

## Discover the endpoints

```sh
curl https://set.world/api/spec
```

```ts
type Spec = { version: 1; endpoints: SpecEntry[] };

type SpecEntry = {
  method: 'GET' | 'POST';
  path: string;                                      // ":name" marks a path segment
  query?: string;                                    // the parameters, in prose
  example?: string;                                  // a path you can call as written
  summary: string;
  scenario: { use: string; returns: string };        // labels for people
  workspace: { title: string; description: string }; // labels for people
};
```

One entry, with its copy text left out:

```json
{
  "method": "GET",
  "path": "/api/roll/item",
  "workspace": { "title": "…", "description": "…" },
  "query": "seed?",
  "summary": "…",
  "scenario": { "use": "…", "returns": "…" }
}
```

`query` is prose, not a schema; the skill that owns each endpoint has its schema.

## Requests

- **Method.** `GET`, no body. Structured parameters (`override`, `actors`) are URL-encoded JSON.
- **CORS.** Open for `GET`, `POST` and `OPTIONS` from any origin, so a browser game can call Set directly.
- **Vectors.** Stats and item vectors are dash-joined integers: `stats=14-12-10-11-13-16-12-14-11`, `rolls=` with 19 numbers.
- **Repeats.** A repeated parameter uses its first value.
- **Malformed is an error, not an omission.** `seed=abc` returns 400. An empty `seed=` means unseeded; every other empty value is invalid.

## Responses and errors

Successes return the payload directly, never wrapped. Failures return one envelope:

```ts
type ErrorEnvelope = { error: { code: string; message: string; field?: string } };
```

```json
{ "error": { "code": "INVALID_SEED", "message": "seed must be an integer in [0, 4294967295]", "field": "seed" } }
```

| Status | Meaning |
| --- | --- |
| 400 | Invalid input; `field` names the parameter |
| 404 | Unknown guide, skill or table |
| 405 | Wrong method (guide, skill, combat and MCP routes) |
| 409 | A `contentVersion` or `rulesVersion` precondition that no longer matches the server |

## Seeds, `contentVersion` and replay

- `seed` is an unsigned 32-bit integer, 0 to 4294967295. Omit it for a random result; the response then carries `"seed": null`.
- Every seeded response carries `seed` and `contentVersion`. The same operation, effective inputs, seed and `contentVersion` reproduce the same object.
- `contentVersion` is `4.<fingerprint>` (now `4.swpi78HUCtcrZ1Eoi9dipw`): a rules revision, then a 22-character base64url fingerprint (128 bits of SHA-256) of the item tables, skills, advantages, disadvantages and classes. A table edit moves the fingerprint; an output-affecting code change bumps the revision.
- Old snapshots are not hosted. Keep full objects when you need history; a seed is not a save file.
- To expand one seed into many, see [Rolling a group](https://set.world/skills/rolling-a-group).

What to store with anything you generate:

```ts
type Replay = {
  operation: string;                  // "/api/roll/character"
  inputs: Record<string, string>;     // every effective parameter except the seed
  seed: number | null;
  contentVersion: string;             // "4.swpi78HUCtcrZ1Eoi9dipw"
};
```

## One soul per call

Every creating request (roll, craft, drop, combat damage) waits in one process-local queue after validation. A busy server makes the next caller wait; that wait is the throttle. There is no `count=` parameter and no batch endpoint, so send roster requests one at a time.

## Tables as CSV

Reference endpoints return JSON by default and CSV with `format=csv`: RFC 4180, a header row, CRLF line ends, quoted fields where needed, and a space between the values of a list inside one cell. Any other `format` returns 400 with `INVALID_FORMAT`.

Each table's own skill shows its CSV call and columns.

## Query a table

All of Set's data is served by an endpoint, and the list endpoints take filters, so a game can ask for one row instead of the table:

| Filter | Matches | On |
| --- | --- | --- |
| `name=` | The exact name, ignoring case | `/api/skills`, `/api/classes`, `/api/advantages`, `/api/disadvantages`, `/api/effects`, `/api/tables/:table` |
| `id=` | The row's `id` | `/api/skills`, `/api/advantages`, `/api/disadvantages` |
| `index=` | The entry's `index` | `/api/tables/:table` |
| `mainhand=` | The class bound to that weapon | `/api/classes` |
| `tier=`, `primary=`, `secondary=` | Traits by their impact | `/api/advantages`, `/api/disadvantages` |
| `q=` | Text anywhere in the name or prose, ignoring case | All of the above |

```sh
curl 'https://set.world/api/classes?mainhand=sword'
```

```json
{
  "count": 1,
  "data": [{ "name": "Adventurer", "flavor": "The Adventurer arrived upon these shores seeking a tale; what was found is a story that permits no departure", "mainhand": "sword" }]
}
```

Filters combine with each other and with `format=csv`. The response keeps the shape of the unfiltered one; a filter that matches nothing returns an empty list, and an empty filter value returns 400 with `INVALID_QUERY`. The map of every data endpoint is in [AGENTS.md](https://set.world/AGENTS.md).

## Connect over MCP

`https://set.world/mcp` is a Model Context Protocol server: Streamable HTTP, JSON responses, no session and no key. It runs no model; it forwards to the same endpoints and skills.

```sh
claude mcp add --transport http set https://set.world/mcp
```

```ts
type SetApiInput = { path: string; query?: Record<string, unknown> };          // path: a GET path from /api/spec
type SetSkillInput = { name: string };                                         // a skill slug, such as "rolling-an-item"
type DeriveSeedInput = { seed: number; index?: number; count?: number };       // count up to 256

type ToolResult = { content: [{ type: 'text'; text: string }]; isError?: true };
```

| Tool | Returns in `text` |
| --- | --- |
| `set_api` | The endpoint's own response body. In `query`, an array of numbers is dash-joined (`stats`, `rolls`) and any other object is sent as JSON (`override`, `actors`). `isError` is set when the endpoint answered with an error |
| `set_skill` | The skill's Markdown |
| `derive_seed` | `{ "seed": 7, "seeds": [{ "index": 0, "seed": 7 }, { "index": 1, "seed": 2654435776 }] }` |

The skills are also MCP resources (`resources/list`, `resources/read`) under their `https://set.world/skills/<name>` URLs. A call by hand:

```sh
curl -s https://set.world/mcp -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_api","arguments":{"path":"/api/roll/item","query":{"seed":42}}}}'
```

```json
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "{\"name\":\"…\",\"slot\":\"tool\", …}" }] } }
```

## What the API never does

It stores no wallets, inventories, levels or combat state, and has no second currency. The one write is the page route `/roll/character`, which saves an immutable birth certificate ([Rolling a character](https://set.world/skills/rolling-a-character)).
