# SDK reference

`@zenitya/sdk` is the only package a project installs. It is MIT licensed and
published on npm.

```sh
bun add @zenitya/sdk
```

```ts
import { Project, type ToolContext } from "@zenitya/sdk";
import { s } from "@zenitya/sdk/schema";
```

## `Project`

The registry your entrypoint exports as its default export.

```ts
const project = new Project();
export default project;
```

### `project.tool(key, config)`

Registers a tool and returns the project, so calls can be chained. Throws when
the key is taken, invalid, or the project already has 128 tools.

| Field          | Type                                            | Required | Notes                                    |
| -------------- | ----------------------------------------------- | -------- | ---------------------------------------- |
| `title`        | `string`                                        | yes      | At most 120 characters                   |
| `description`  | `string`                                        | yes      | At most 2,000 characters; agents read it |
| `schema`       | `s.object(...)`                                 | yes      | The input; always an object              |
| `outputSchema` | any `s` schema                                  | no       | Validated against what `execute` returns |
| `hints`        | `ToolHints`                                     | no       | See below                                |
| `execute`      | `(input, context) => Output \| Promise<Output>` | yes      | `input` is typed from `schema`           |

`key` starts with a letter and has only letters, digits and underscores, at
most 64 characters.

### `ToolHints`

All optional booleans, as in MCP tool annotations: `readOnlyHint`,
`destructiveHint`, `idempotentHint`, `openWorldHint`. A tool cannot be both
read-only and destructive.

## `ToolContext`

The second argument of `execute`.

| Field          | Type          | Notes                            |
| -------------- | ------------- | -------------------------------- |
| `invocationId` | `string`      | Unique per call                  |
| `signal`       | `AbortSignal` | Aborts when the call times out   |
| `logger`       | `ToolLogger`  | `debug`, `info`, `warn`, `error` |

Each logger method takes `(fields: JsonObject, message?: string)`:

```ts
context.logger.warn({ orderId, status: response.status }, "carrier lookup failed");
```

## `s`: schema builders

Every builder returns a schema that is both the JSON Schema agents see and the
TypeScript type of the value.

| Builder                          | JSON Schema                                          | TypeScript         |
| -------------------------------- | ---------------------------------------------------- | ------------------ |
| `s.string()`                     | `{ type: "string" }`                                 | `string`           |
| `s.number()`                     | `{ type: "number" }`                                 | `number`           |
| `s.integer()`                    | `{ type: "integer" }`                                | `number`           |
| `s.boolean()`                    | `{ type: "boolean" }`                                | `boolean`          |
| `s.enum("a", "b")`               | `{ enum: ["a", "b"] }`                               | `"a" \| "b"`       |
| `s.object({ ... })`              | object, all properties required, no extra properties | `{ readonly ... }` |
| `s.array(item, { minItems? })`   | `{ type: "array", items }`; `minItems` is 0 or 1     | `readonly Item[]`  |
| `s.anyOf(a, b, ...)`             | `{ anyOf: [...] }`                                   | union              |
| `s.ref("#/properties/customer")` | `{ $ref }`, local pointers only                      | JSON value         |

String formats, each `{ type: "string", format }` and typed as `string`:
`s.date()`, `s.time()`, `s.datetime()`, `s.duration()`, `s.email()`,
`s.uuid()`, `s.hostname()`, `s.ipv4()`, `s.ipv6()`.

`s.enum` takes all strings or all finite numbers, unique, at least one.

### Modifiers

- `.describe(text)`: adds the description agents read. At most 2,000
  characters.
- `.nullable()`: allows `null` as well. Properties are always required, so this
  is how a value becomes optional: the caller passes `null`.

```ts
const schema = s.object({
  query: s.string().describe("Text to search for."),
  status: s.enum("open", "closed").nullable().describe("Filter by status, or null for any."),
  tags: s.array(s.string(), { minItems: 1 }).describe("At least one tag."),
});
```

### References

`s.ref(path)` points at another node of the same schema with a local JSON
Pointer. The target must exist in the final schema:

```ts
const schema = s.object({
  customer: s.object({ id: s.string() }),
  previousCustomer: s.ref("#/properties/customer"),
});
```

### Limits

A schema may be at most 256 KiB, 32 levels deep and 10,000 nodes. Tool input
and output are each at most 1 MiB of JSON, nested at most 64 levels.

## Types

`InferSchema<typeof schema>` gives the TypeScript type of a schema. `JsonValue`,
`JsonObject`, `JsonArray` and `JsonPrimitive` describe JSON values.
