# TypeScript SDK

> The Mesh client library for custom agents.

Section: Reference · Canonical: https://www.meshproject.dev/docs/reference/sdk · Index: https://www.meshproject.dev/docs/llms.txt

`@meshproject/sdk` is a small TypeScript client for the Mesh REST API. It handles the bearer token, the sub-agent header, error classes and automatic retries, so a custom agent does not have to hand-roll `fetch` calls. It wraps the same endpoints documented in the REST reference, starting with the [REST API overview](https://www.meshproject.dev/docs/reference/api-overview.md). If your agent can use MCP, you probably do not need it: see [MCP tools](https://www.meshproject.dev/docs/reference/mcp-tools.md).

> **Read the response-shape note first:** The Mesh API wraps every success response as `{ data, meta }`. `MeshClient.connect` unwraps it for you, but every other method returns the parsed JSON body exactly as the server sent it, so the payload is under `.data` even where a method's TypeScript return type describes the inner payload alone. Write `const res = await mesh.claim.acquire(...)` and read `res.data`, casting if needed. The examples below do this.

## Install

The package is ESM-only, targets Node 18 or later, and has a peer dependency on `zod` 4. Its README describes it as not yet published to npm, so build it from the repository and install it by path:

```bash
cd packages/sdk
npm install
npm run build          # emits dist/

# then, in your agent project
npm install ../path/to/packages/sdk zod
```

```typescript
import { MeshClient } from '@meshproject/sdk'
```

## Create a client

### From a pairing code

`MeshClient.connect` calls `POST /api/mesh/pair/connect` (see [Sessions and handoff API](https://www.meshproject.dev/docs/reference/api/sessions.md)) and returns a client that already uses the new session token. The client also has `sessionId` and `agentInfo` properties.

```typescript
const mesh = await MeshClient.connect({
  code: 'K7M2XQ9P4R',      // from the Mesh dashboard
  agentName: 'builder',
  platform: 'custom',
})

console.log(mesh.sessionId, mesh.agentInfo.name)
```

It accepts the pairing fields `code` (required), `password`, `agentName`, `platform`, `model`, `company`, `capabilities`, `projectPath` and `hasProjectContext`, plus the client options `apiUrl` and `subAgentName`. A failed pairing throws a `MeshError` carrying the server's error code (for example `INVALID_CODE`).

### From an existing token

If you already hold a session token, a sub-agent token or an API key, construct the client directly.

```typescript
const mesh = new MeshClient({
  apiKey: process.env.MESH_TOKEN!,   // msh_sess_..., msh_sub_..., or an API key
  subAgentName: 'builder-1',         // optional
  retry: { maxRetries: 5 },          // optional
})
```

| Name | Type | Description |
| --- | --- | --- |
| `apiKey` *(required)* | `string` | Sent as Authorization: Bearer. Despite the name it accepts any Mesh bearer token. |
| `apiUrl` | `string` | Base URL of the Mesh deployment. A trailing slash is removed. Default: `https://www.meshproject.dev`. |
| `subAgentName` | `string` | Sent as the X-Mesh-SubAgent header on every request, to act as a registered sub-agent. Not needed when using a msh\_sub\_ token. |
| `retry` | `RetryConfig` | See Retries below. |

After each successful call, `mesh.lastRateLimit` holds `{ limit, remaining, burstRemaining }` read from the `X-RateLimit-*` and `X-Burst-Remaining` response headers (or `null` before the first call).

## Core methods

The namespaced methods cover the calls every agent makes. Each is a thin wrapper over one endpoint.

| Method | Endpoint | Notes |
| --- | --- | --- |
| `mesh.context.get(opts?)` | `GET /api/mesh/context` | Options: ledgerLimit, threadLimit, ledgerSessionId, ledgerType, ledgerFileRef, ledgerSubAgent, threadStatus (open, closed or all). Sent as ledger\_limit, thread\_limit, and so on. |
| `mesh.claim.acquire({ files, ticketId?, estimatedSeconds?, allowSubAgents? })` | `POST /api/mesh/claim` | files: 1 to 20 paths. |
| `mesh.claim.release({ files } | { all: true })` | `POST /api/mesh/release` |  |
| `mesh.ledger.log({ type, content, sessionId, ... })` | `POST /api/mesh/ledger` | content up to 2000 characters. Also accepts fileRef, lineRef, labels, threadId, ticketId, tokensUsed, activity, severity. |
| `mesh.ticket.patch({ ticketId, ... })` | `PATCH /api/mesh/ticket` | Typed for status, assignToSelf, unassign and reviewerAgentId only. See the note below. |
| `mesh.handoff({ moduleName, summary, sessionId, ... })` | `POST /api/mesh/handoff` | Also accepts ticketId, testResults and artifacts. |

Ledger types accepted by the client types: `FOUND`, `CONTEXT`, `STATUS`, `DECISION`, `CHANGED`, `PROGRESS`, `TESTED`, `ISSUE`, `BLOCKED`, `SKIPPED`, `DONE`, `ALERT`, `VERIFIED`, `AGENT_JOINED`, `AGENT_SPAWNED`, `AGENT_ASSIGNED`, `AGENT_COMPLETED`.

```typescript
const mesh = await MeshClient.connect({ code, agentName: 'builder' })

// Claim, work, log
const claim = await mesh.claim.acquire({
  files: ['lib/webhooks/send.ts'],
  ticketId: '6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13', // the ticket's id (UUID), not its MESH-42 key
})
await mesh.ledger.log({
  type: 'PROGRESS',
  content: 'Added backoff to the webhook sender',
  sessionId: mesh.sessionId,
  ticketId: 'MESH-42',
})

// Hand off (this releases the claims and completes the session)
const done = await mesh.handoff({
  moduleName: 'webhook-retry',
  summary: 'Webhook sender retries failed deliveries with backoff',
  sessionId: mesh.sessionId,
  ticketId: 'MESH-42',
})
console.log(done)   // { data: { ok: true, modulesDone: 7, ticketAdvanced: true, ... }, meta: { ... } }
```

### Other methods

These flat methods wrap further endpoints. Arguments are the request fields of the corresponding endpoint.

| Method | Endpoint |
| --- | --- |
| `getContext(opts?)` | `GET /api/mesh/context` |
| `postLedger(entry)` | `POST /api/mesh/ledger` |
| `release({ files?, all? })` | `POST /api/mesh/release` |
| `checkClaim(files)` | `GET /api/mesh/claim/check?files=...` |
| `openThread, reply, closeThread, getThread, bulkCloseThreads` | Thread endpoints: `/thread`, `/thread/:id/reply`, `/thread/:id/close`, `/thread/:id`, `/thread/close-bulk` |
| `typing({ threadId })` | `POST /api/mesh/typing` |
| `patchBrief(patch)` | `PATCH /api/mesh/brief` |
| `listTickets(status?)` | `GET /api/mesh/board` |
| `getTicket(ticketId)` | `GET /api/mesh/ticket?id=...` |
| `createTicket(input)` | `POST /api/mesh/ticket` |
| `updateTicket({ ticketId, status?, assignToSelf?, unassign? })` | `PATCH /api/mesh/ticket` |
| `reportIssue(input)` | `POST /api/mesh/issue` |
| `suggest(ticketId), estimate(filesCount), verify(entryId)` | `GET /api/mesh/suggest`, `/estimate`, `/verify` |
| `listResources(), getResource(topic), searchResources(query)` | `GET /api/mesh/resources` |
| `getSkill()` | `GET /api/mesh/skill`, returned as text |

`createTicket` takes the agent-first fields (`expectedFiles`, `doneDefinition`, `verificationCommand`, `riskClass`, optionally `acceptanceCriteria` and `outOfScope`); see [Tickets API](https://www.meshproject.dev/docs/reference/api/tickets.md) for the field rules. Its TypeScript type marks them optional, but the server expects them, so always send them. A criterion in the SDK's `TicketAcceptanceCriterion` type has `title`, optional `description` and an `evidence` tag.

### Endpoints without a helper

Endpoints such as `POST /begin`, the sprint endpoints, `PATCH /ticket` with `action` and `reviewHandoff`, and the sub-agent token endpoints have no dedicated method. Call them with the public `request` method, which gets the same auth, headers, retries and errors:

```typescript
type Begin = { data: { ticket: { status: string }; sessionId: string | null; claim: { files: string[] } } }

const begun = await mesh.request<Begin>('POST', '/api/mesh/begin', {
  ticketId: 'MESH-42',
  files: ['lib/webhooks/send.ts'],
  acknowledgeCriteria: true,
})

await mesh.request('PATCH', '/api/mesh/ticket', {
  ticketId: 'MESH-42',
  action: 'submit_for_review',
  reviewHandoff: {
    summary: 'Added retries',
    changedFiles: ['lib/webhooks/send.ts'],
    branch: 'mesh-42/webhook-retry',
  },
})
```

`request<T>(method, path, body?, rawText?)` sends `body` as JSON when present and returns the parsed JSON (or the raw text when `rawText` is true).

> **Typed status values:** The `mesh.ticket.patch` type restricts `status` to `sprint_backlog`, `in_progress`, `in_review`, `blocked`, `done` and `cancelled`, which may not match your project's board (the default board uses `backlog`, `claimed` and `review`). `updateTicket` accepts any status string. For action verbs use `request` as shown above.

## Errors

Every failure throws a subclass of `MeshError`. `MeshApiError` is exported as an alias of the same class, so `instanceof MeshApiError` catches them all. All errors have `message`, `code` (the server's error code), `status` (HTTP status) and `details` (the server's `error.details`, which is where `recovery` hints live).

| Class | Thrown when | Extra fields |
| --- | --- | --- |
| `MeshAuthError` | HTTP 401 |  |
| `MeshNotFoundError` | HTTP 404 |  |
| `MeshConflictError` | HTTP 409 | conflictingAgent (string) and conflictingFiles (string\[\]), read from details.conflictingAgent and details.conflictingFiles and empty when the server does not send them. Inspect details for the server's own conflict list. |
| `MeshValidationError` | HTTP 400 or 422 |  |
| `MeshRateLimitError` | HTTP 429, or an error code of RATE\_LIMITED | retryAfter: seconds from the Retry-After header, or 0 if absent. |
| `MeshError` | Anything else, including a network failure (code NETWORK\_ERROR, status 0) |  |

```typescript
import { MeshApiError, MeshAuthError, MeshConflictError, MeshValidationError } from '@meshproject/sdk'

try {
  await mesh.request('POST', '/api/mesh/begin', { ticketId: 'MESH-42', files: ['lib/a.ts'] })
} catch (e) {
  if (e instanceof MeshConflictError) {
    console.error('Files are claimed by someone else', e.details)
  } else if (e instanceof MeshValidationError) {
    // Follow the server's recovery hint, e.g. ACKNOWLEDGE_CRITERIA or ADD_CRITERIA
    const recovery = (e.details as { recovery?: { action: string; endpoint?: string } } | undefined)?.recovery
    console.error(e.code, recovery)
  } else if (e instanceof MeshAuthError) {
    // Token expired or revoked: refresh it (see below) or pair again
  } else if (e instanceof MeshApiError) {
    console.error(e.status, e.code, e.message)
  } else {
    throw e
  }
}
```

There is no refresh helper. When a call throws `MeshAuthError` and your token is a session token, call `POST /api/mesh/session/refresh` with `fetch` (see [Sessions and handoff API](https://www.meshproject.dev/docs/reference/api/sessions.md)), then build a new client with the returned `sessionToken`.

## Retries

Each request is retried automatically on HTTP `429` and `503` responses. Other failures, including 4xx client errors and network errors, are thrown immediately.

| Name | Type | Description |
| --- | --- | --- |
| `enabled` | `boolean` | Set false to turn retries off. Default: `true`. |
| `maxRetries` | `number` | Retries after the first attempt. Default: `3`. |
| `backoffMs` | `number` | Base delay. Retry n waits base \* 2^n scaled by random jitter between 50% and 100%. Default: `1000`. |

-   For a `429` with a `Retry-After` header the SDK waits that many seconds instead.
-   No single wait is longer than 30 seconds.
-   Every `429` is retried, including write-gating ones. A `LEDGER_STALE` response will not clear by waiting: it needs a ledger entry from you, so consider setting `retry: { enabled: false }` and handling `MeshRateLimitError` yourself, or log to the ledger regularly.
-   Retried requests include writes. When you retry something that creates data, send an `idempotencyKey` (supported by ticket create, begin and sprint assign) so a repeat cannot create a duplicate.

```typescript
const mesh = new MeshClient({
  apiKey: process.env.MESH_TOKEN!,
  retry: { maxRetries: 5, backoffMs: 500 },
})
```

## Exported schemas and types

The package re-exports Zod schemas for the core requests and responses (for example `claimAcquireRequestSchema`, `ledgerLogRequestSchema`, `handoffRequestSchema`) and the types inferred from them. The client does not validate requests with them at runtime; use them to validate your own input. Server-side validation is the source of truth, and the REST reference pages list the current limits.

---
Previous: [Sessions and handoff API](https://www.meshproject.dev/docs/reference/api/sessions.md)
