# typeship: full documentation > ## Documentation index > Fetch the complete documentation index at https://typeship.dev/llms.txt (every page, one line each) or the full text at https://typeship.dev/llms-full.txt. > Append .md to any docs URL, or send Accept: text/markdown, for the markdown twin of that page. Every section below is one page; each names its source URL, and https://typeship.dev/llms.txt lists them. # Overview Source: https://typeship.dev/docs.md typeship turns your OpenAPI or GraphQL spec into the SDK, CLI, and MCP server your users install, and keeps them current with a pull request on every spec change. typeship generates the client-side of your API and maintains it. Point a project at your spec, and typeship produces a typed SDK with zero runtime dependencies in TypeScript, Python, or Go, plus a CLI and an MCP server for the TypeScript package. When the spec changes, typeship regenerates everything and opens a pull request per language with the API changes written out. Examples use the Acme API throughout, so you will see `acme`, `AcmeClient`, and `ACME_TOKEN`. Your names come from your spec. ## Start here - [Quickstart](https://typeship.dev/docs/quickstart): Create a project, generate your first package, make a call. Ten minutes. - [Concepts](https://typeship.dev/docs/concepts): Projects, platforms, languages, generations, and how they fit together. - [Regeneration](https://typeship.dev/docs/projects/regeneration): How a spec change becomes a pull request, and what is in it. - [typeship API](https://typeship.dev/docs/typeship-api/api): Drive typeship from a pipeline or an agent. ## What you get - [SDK](https://typeship.dev/docs/platforms/sdk): Typed results and errors, auto-pagination, retries, hooks, validation, webhooks, streaming. TypeScript, Python, Go. - [CLI](https://typeship.dev/docs/platforms/cli): Every operation as a command with typed flags and JSON output, plus login, config, docs, and completion. - [MCP server](https://typeship.dev/docs/platforms/mcp): Every operation as a tool for coding agents, with docs tools and a hosted endpoint. ## Drive typeship itself - [typeship CLI](https://typeship.dev/docs/cli): npm install -g typeship-ax. Every console action as a command, JSON out, built for agents and pipelines. - [typeship SDKs](https://typeship.dev/docs/sdks): typeship-ax on npm, typeship on PyPI, github.com/typeship-ax/go. Generated from typeship's own spec. - [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp): Hosted at typeship.dev/mcp and in the MCP registry as dev.typeship/typeship; local over stdio from the npm package. ## Try it without an account The [generator on the homepage](https://typeship.dev/) runs on any spec without signing up, capped at one platform and the first 25 operations, and keeps nothing. It is the fastest way to see what your package will look like. Everything that keeps a package current happens in a project. --- # Quickstart Source: https://typeship.dev/docs/quickstart.md Create a project, generate your first package, install it, and make a call. Then link the spec so it stays current. By the end of this page you will have a generated TypeScript SDK for your API installed in a project and making calls, and a typeship project that regenerates it when the spec changes. You need a spec at a URL typeship can fetch: Swagger 2.0, OpenAPI 3.0 or 3.1, or a GraphQL schema or endpoint. If you do not have one handy, any public OpenAPI URL works for the walkthrough. The walkthrough uses the console. Every step is also a command, shown in the agent notes: install the [typeship CLI](https://typeship.dev/docs/cli) with `npm install -g typeship-ax` (or run it without installing, `npx -y typeship-ax@latest ...`), then `typeship login`. 1. **Create a project** Sign in to the [console](https://typeship.dev/console) and choose **new project**. Give it a name, paste the spec URL, and pick platforms. The TypeScript SDK is on by default. Add **CLI** and **MCP server** if you want them in this walkthrough. The name matters more than it looks. Package and repository defaults derive from it, so name the project after the API: "Acme API", not "test". > **For AI agents:** `typeship projects create --name "Acme API" --spec-url https://api.acme.example.com/openapi.json --languages '["typescript"]' --platforms '["sdk","cli"]'` > > (needs > > `TYPESHIP_TOKEN` > > ). No account yet? > > `typeship generate run --spec '{"url":"..."}' --out sdk/` > > generates without one, and the runbook at > > [/agents.md](https://typeship.dev/agents.md) > > covers the rest. 2. **Generate** On the project page, choose **generate now**. typeship fetches the spec, generates, and opens the generation: every file, browsable, with the warnings for anything it skipped or approximated. Read the warnings once. They tell you what the spec left out. Choose **download .zip** to get the package. > **For AI agents:** `typeship projects generate > generations.json` > > , then write each > > `data[].files[]` > > entry to disk; or, for a one-off, > > `typeship generate run ... --out sdk/` > > writes the files directly. 3. **Install and make a call** Unzip the package into your repository and build it once: ```bash unzip acme.zip -d vendor/acme cd vendor/acme && npm install && npm run build && cd - npm install ./vendor/acme ``` Then call your API: ```ts import { AcmeClient, unwrap } from "acme"; const client = new AcmeClient({ bearerToken: process.env.ACME_TOKEN! }); const result = await client.accounts.list({ limit: 5 }); if (!result.ok) throw result.error; for await (const account of client.accounts.list()) { console.log(account.id); } ``` Your resource and method names come from your spec. `api.md` in the package lists every one of them. If you generated the CLI, it is on your PATH now: ```bash acme login --token "$ACME_TOKEN" acme accounts list --limit 5 ``` 4. **Keep it current** Open the project's settings and set a destination: a repository for the TypeScript package, such as `acme/acme-node`, and optionally a directory. Install the typeship GitHub App on that repository when prompted. Then decide how typeship notices changes. Auto-regen is on by default: * If the spec lives in a GitHub repository, switch the source to **github repo** with the repository and path. Pushes that touch the spec regenerate the package. * If the spec is served at a URL, nothing more to do. typeship polls every 30 minutes. From now on, every spec change becomes a pull request in the destination repository, with the API changes listed in the body. Review and merge. See [Regeneration](https://typeship.dev/docs/projects/regeneration). > **For AI agents:** `typeship projects update --destinations '{"typescript":{"repo":"acme/acme-node","directory":"."}}' --auto-regen true` > > ; a repository source is > > `--source '{"kind":"repo","repo":"acme/api","path":"openapi.yaml"}'` > > . Installing the GitHub App is a browser step for the user; say so and give them the console link. ## Where next * Add languages: [Python](https://typeship.dev/docs/platforms/sdk/python) and [Go](https://typeship.dev/docs/platforms/sdk/go) get their own packages and pull requests. * Give agents your API: wire the [MCP server](https://typeship.dev/docs/platforms/mcp) into Claude Code with `acme mcp --claude`, or turn on the [hosted endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint). * Publish under your name: [Publish your packages](https://typeship.dev/docs/guides/publish). * Fix a spec you cannot edit: [Spec patches](https://typeship.dev/docs/projects/spec-patches). * Automate typeship itself: [typeship API](https://typeship.dev/docs/typeship-api/api). --- # Concepts Source: https://typeship.dev/docs/concepts.md The handful of nouns typeship uses, and how they relate: spec, project, platform, language, generation, destination, and the packages that come out. ## Spec Your API's contract: a Swagger 2.0, OpenAPI 3.0, or 3.1 document, or a GraphQL schema. typeship reads it and never writes to it. There are no vendor extensions to add. Everything typeship needs beyond the spec lives in the project. ## Project One spec and its lineage. A project holds where the spec lives, which languages and platforms to generate, package names and destinations, spec patches, config, and the history of every generation and every spec version. See [Projects](https://typeship.dev/docs/projects/). ## Platform One thing a project generates from the spec, with a switch each in project settings: | Platform | What it is | | -------- | ---------------------------------------------------------------------------------------------------- | | `sdk` | A typed client, in TypeScript, Python, or Go. Each language is its own platform. At least one is on. | | `cli` | Every operation as a command. | | `mcp` | Every operation as a tool for coding agents. | The CLI and MCP server are built on the TypeScript SDK and ship inside the TypeScript package, so they keep that SDK on. Pro is priced per platform. Turning a platform off stops generating it; nothing already delivered is removed. ## Language The ecosystem a package is generated for: TypeScript, Python, or Go. A project generates one package per language, each written in that language's idiom rather than translated from a template. Python and Go packages are SDK-only. ## Package What a generation produces for one language: a complete, publishable package with readable source and zero runtime dependencies. Its name derives from your API's title unless you set one. It carries `api.md`, a full surface reference, and `AGENTS.md`, context for coding agents. ## Generation One run of the generator for one language, recorded with its trigger, warnings, and files. A generation is reproducible from its spec version. Generations are triggered manually, by a push to the spec, by the URL poller, or as a preview on a pull request. ## Spec version The exact spec text a generation came from, content-addressed by hash and kept per project. Read them back through the typeship API for diffs and audits. ## Destination Where a language's package lands: a repository and, optionally, a directory. Regeneration opens pull requests there. Dedicated repositories per language (`acme/acme-node`, `acme/acme-python`, `acme/acme-go`) are the recommended shape. ## Regeneration The loop that keeps packages current: detect a spec change, generate every language, open one pull request per language with the API changes spelled out. See [Regeneration](https://typeship.dev/docs/projects/regeneration). --- # Overview Source: https://typeship.dev/docs/platforms/sdk.md A typed client for your API with zero runtime dependencies, in TypeScript, Python, or Go. Typed errors, auto-pagination, retries, hooks, and validation, generated from your spec. The `sdk` platform is the foundation of every package typeship generates. It is always on. In TypeScript, the CLI and MCP server both import it. Python and Go packages are SDK-only. This page uses TypeScript for the primary examples and shows the Python and Go equivalents where the shape differs. The idioms differ on purpose. Each SDK is written the way a developer in that language expects, not translated from one shared template. For the full language-specific surface, see [Python](https://typeship.dev/docs/platforms/sdk/python) and [Go](https://typeship.dev/docs/platforms/sdk/go). Examples use the Acme API, whose package is `acme`. Your names come from your spec's title. ## What ships in the package **TypeScript** ```text acme/ package.json zero runtime dependencies, ESM, Node 18+ src/index.ts AcmeClient, ClientOptions, environments, VERSION src/types.ts every schema as an interface or type src/errors.ts per-status error classes and per-operation unions src/resources/*.ts one module per resource src/core/ HTTP runtime and pagination src/webhooks.ts when the spec declares webhooks src/schemas.ts validation tables, plain data tests/*.test.mjs one smoke test per operation, node:test only api.md the full surface reference in one file AGENTS.md context for coding agents ``` The package ships as readable TypeScript source and compiles with `npm run build`. `main`, `types`, and `exports` point into `dist/`. **Python** ```text acme/ pyproject.toml dependencies = [], requires-python >= 3.11 acme/__init__.py AcmeClient, errors, models, webhooks acme/models.py TypedDicts and Literal enums acme/resources/*.py one module per resource acme/_core.py urllib runtime, retries, pagination acme/webhooks.py when the spec declares webhooks acme/py.typed api.md AGENTS.md ``` **Go** ```text acme-go/ go.mod no require block, go 1.21 client.go New, Option funcs, service fields models.go structs, enums, params types accounts.go ... one file per resource core.go net/http runtime, retries pagination.go Iter[T] webhooks.go when the spec declares webhooks api.md AGENTS.md ``` Every file passes `gofmt`. ## Create a client **TypeScript** ```ts import { AcmeClient } from "acme"; const client = new AcmeClient({ bearerToken: process.env.ACME_TOKEN!, }); ``` The TypeScript client does not read credentials from the environment. Pass them explicitly. This keeps the client safe in browsers and edge runtimes, where `process.env` may not exist. The generated CLI and MCP server are the pieces that read `ACME_TOKEN`. **Python** ```python from acme import AcmeClient client = AcmeClient(bearer_token=os.environ["ACME_TOKEN"]) # or, with ACME_TOKEN set in the environment: client = AcmeClient() ``` **Go** ```go import acme "github.com/acme/acme-go" client, err := acme.New(acme.WithBearerToken(os.Getenv("ACME_TOKEN"))) // or, with ACME_TOKEN set in the environment: client, err := acme.New() ``` ### Client options Every option is optional when the spec pins a server URL. If it does not, `baseUrl` becomes a required argument and generation warns you. **TypeScript** ```ts const client = new AcmeClient({ baseUrl: "https://api.acme.example.com/v1", // defaults to the spec's first server bearerToken: process.env.ACME_TOKEN!, timeoutMs: 30_000, // per attempt maxRetries: 2, // retries after the first attempt defaultHeaders: { "Request-Source": "billing" }, fetch: myFetch, // bring your own: proxies, tests, instrumentation onRequest: (context) => {}, onResponse: (response, context) => {}, onError: (error, request) => {}, debug: false, validate: false, }); ``` **Python** ```python client = AcmeClient( base_url="https://api.acme.example.com/v1", bearer_token=token, timeout=30.0, max_retries=2, default_headers={"Request-Source": "billing"}, transport=my_transport, # swap urllib for anything with the same signature on_request=..., on_response=..., on_error=..., debug=False, validate=False, ) ``` **Go** ```go client, err := acme.New( acme.WithBaseURL("https://api.acme.example.com/v1"), acme.WithBearerToken(token), acme.WithTimeout(30*time.Second), acme.WithMaxRetries(2), acme.WithHTTPClient(httpClient), acme.WithOnRequest(func(r *http.Request) {}), acme.WithOnResponse(func(r *http.Response) {}), acme.WithOnError(func(err error, method, path string) {}), acme.WithDebug(func(e acme.DebugEvent) {}), acme.WithValidation(acme.ValidateOff), ) ``` ### Authentication Which auth options exist depends on the security schemes in your spec. | Spec declares | TypeScript | Python | Go | | ----------------------------------- | ------------------------------------ | ---------------------- | ---------------------------------------- | | HTTP bearer, OAuth2, OpenID Connect | `bearerToken` | `bearer_token` | `WithBearerToken`, `WithBearerTokenFunc` | | One API key header | `apiKey` | `api_key` | `WithAPIKey` | | Several API keys | one option per header or query param | one keyword per param | one `With...` per param | | HTTP basic | `basicAuth: { username, password }` | `username`, `password` | `WithBasicAuth` | | OAuth2 client credentials | `clientCredentials` | `client_credentials` | `WithClientCredentials` | Bearer tokens and API keys accept a callback, resolved before every attempt. Use it for credentials that expire: ```ts const client = new AcmeClient({ bearerToken: async () => (await tokenManager.current()).accessToken, }); ``` When the spec declares OAuth2, `clientCredentials` handles the whole token lifecycle. The client posts to the spec's token URL, caches the token until a minute before expiry, and shares one in-flight token request across concurrent calls. An explicit bearer token always wins. ```ts const client = new AcmeClient({ clientCredentials: { clientId: process.env.ACME_CLIENT_ID!, clientSecret: process.env.ACME_CLIENT_SECRET!, scopes: ["read:accounts"], // optional tokenParams: { audience: "https://api.acme.example" }, // if your auth server needs one authMethod: "post", // or "basic" tokenUrl: "https://auth.acme.example/oauth/token", // required only if the spec has none }, }); ``` Python takes `client_credentials={"client_id": ..., "client_secret": ...}`. Go takes `WithClientCredentials(acme.ClientCredentials{ClientID: ..., ClientSecret: ...})`. Both cache and lock the same way. ### Environments When the spec declares two or more servers, the SDK exports them by name, taken from each server's description. The first server stays the default. **TypeScript** ```ts import { AcmeClient, environments } from "acme"; const client = new AcmeClient({ baseUrl: environments.sandbox, bearerToken: token }); ``` **Python** ```python from acme import AcmeClient, ENVIRONMENTS client = AcmeClient(base_url=ENVIRONMENTS["sandbox"], bearer_token=token) ``` **Go** ```go client, err := acme.New(acme.WithBaseURL(acme.ServerSandbox), acme.WithBearerToken(token)) ``` ## Call an operation Operations are grouped into resources by their first tag. Path parameters are positional, in path order. The body comes next, then query and header parameters, then per-call options. **TypeScript** ```ts await client.accounts.get("acct_123"); await client.accounts.create({ name: "Ops", currency: "usd" }); await client.accounts.list({ limit: 50, created: { gte: 1700000000 } }); ``` Method names are de-stuttered. An `operationId` of `createAccount` on the `accounts` resource becomes `client.accounts.create`. The type names follow: `AccountsCreateParams`, `AccountsCreateError`. **Python** ```python client.accounts.get("acct_123") client.accounts.create(name="Ops", currency="usd") # inline body fields become keyword arguments client.accounts.update("acct_123", body={"name": "Ops 2"}) # a $ref body stays one typed argument client.accounts.list(limit=50, created={"gte": 1700000000}) ``` **Go** ```go acct, err := client.Accounts.Get(ctx, "acct_123") created, err := client.Accounts.Create(ctx, &acme.AccountsCreateParams{Name: "Ops"}) // inline body fields are struct fields updated, err := client.Accounts.Update(ctx, "acct_123", acme.AccountParams{Name: "Ops 2"}) // a $ref body is its own argument it := client.Accounts.List(ctx, &acme.AccountsListParams{Limit: acme.Ptr(int64(50))}) ``` Every method takes a `context.Context` first. Params structs are passed by pointer, and `nil` means none. Optional fields are pointers so absent and zero stay distinct; `acme.Ptr(v)` makes one inline. ### Per-call options The last argument of every method overrides the client for that one call. **TypeScript** ```ts await client.accounts.get("acct_123", { timeoutMs: 5_000, maxRetries: 0, headers: { "Request-Source": "cron" }, signal: controller.signal, // composed with the per-attempt timeout }); ``` **Python** ```python client.accounts.get("acct_123", request_options={ "timeout": 5.0, "max_retries": 0, "headers": {"Request-Source": "cron"}, }) ``` **Go** ```go acct, err := client.Accounts.Get(ctx, "acct_123", acme.WithRequestTimeout(5*time.Second), acme.WithRequestMaxRetries(0), acme.WithRequestHeader("Request-Source", "cron"), ) ``` Precedence is the same everywhere: per-call, then per-operation policy set at generation, then the client. ## Results and errors **TypeScript** Nothing throws on HTTP errors. Every call returns a discriminated `ApiResult`, and the error side is a union of the documented error classes for that exact operation: ```ts type ApiResult = | { ok: true; data: T; response: ResponseMeta } | { ok: false; error: E; response?: ResponseMeta }; ``` ```ts import { AcmeClient, NotFoundError } from "acme"; const result = await client.accounts.get("acct_123"); if (!result.ok) { if (result.error instanceof NotFoundError) { result.error.body; // typed body for the documented 404 } throw result.error; // every branch is an Error subclass } result.data; // typed success payload ``` The union for each operation is built from four kinds of class: * Per-status classes such as `NotFoundError`, generated from the spec's documented error responses. Each extends `ApiError` and carries `status`, a typed `body`, and `response` metadata (status, headers, request id). * `UnexpectedApiError` for a status the spec did not document. * `TransportError` when there was no HTTP response at all: network failure, timeout, abort. The message names the request and the deepest cause. * `ValidationError` when [runtime validation](#runtime-validation) is on and a body fails its schema. Prefer exceptions? `unwrap(result)` returns the data or throws the typed error: ```ts import { unwrap } from "acme"; const account = unwrap(await client.accounts.get("acct_123")); ``` **Python** Python raises. Every exception derives from `TypeshipError`, and API errors carry `status`, the parsed `body`, and `request_id`: ```python from acme import ApiError, NotFoundError, TransportError try: account = client.accounts.get("acct_123") except NotFoundError as exc: print(exc.status, exc.body, exc.request_id) except ApiError as exc: ... # any documented or undocumented status except TransportError: ... # no HTTP response ``` Per-status classes such as `NotFoundError` and `UnprocessableEntityError` extend `ApiError`. An undocumented status raises `UnexpectedApiError`. Return values are the parsed JSON, typed as `TypedDict`s, so you read `account["id"]`. **Go** Go returns `(T, error)`. Typed error structs embed `APIError`, so `errors.As` matches at either precision: ```go acct, err := client.Accounts.Get(ctx, "acct_123") if err != nil { var notFound *acme.NotFoundError var apiErr *acme.APIError var transport *acme.TransportError switch { case errors.As(err, ¬Found): // 404, notFound.Body holds the raw JSON case errors.As(err, &apiErr): // any status: apiErr.Status, apiErr.RequestID, apiErr.Decode(&target) case errors.As(err, &transport): // no HTTP response } } ``` ## Pagination List operations with a recognized pagination shape iterate every item across every page, fetching lazily. **TypeScript** Paginated methods return a `PagePromise`. It is both awaitable and async-iterable: ```ts // for await -> every item from every page for await (const account of client.accounts.list({ limit: 50 })) { console.log(account.id); } // await -> one page, as a typed ApiResult const result = await client.accounts.list({ limit: 50 }); if (result.ok) { result.data.items; // Account[] result.data.body; // the raw envelope result.data.hasNextPage(); // boolean await result.data.getNextPage(); // next Page, or null on the last one } ``` Iteration throws the operation's typed error if any page fetch fails. **Python** Paginated methods return a generator. A `_page` sibling returns one raw page: ```python for account in client.accounts.list(limit=50): print(account["id"]) page = client.accounts.list_page(limit=50) # {"data": [...], "next_cursor": ...} ``` **Go** Paginated methods return an `*Iter[T]` with `Next`, `Value`, and `Err`, the shape of `bufio.Scanner` and `sql.Rows`: ```go it := client.Accounts.List(ctx, nil) for it.Next() { account := it.Value() fmt.Println(account.ID) } if err := it.Err(); err != nil { return err } ``` Per-call options passed to `List` apply to every page fetch. Four pagination styles are detected from the spec: | Style | Detected from | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cursor` | A cursor query param (`cursor`, `page_token`, `after`, `starting_after`, ...) plus a next-cursor field in the response (`next_cursor`, `next_page_token`, ..., including inside a `meta` or `pagination` container) | | `cursorFromLastId` | A cursor param plus a `has_more` field, where items have an `id`. The next request cursors from the last item's id. | | `page` | A `page` or `page_number` query param. Advances while pages look full. | | `offset` | An `offset`, `skip`, or `start` query param. Advances by the number of items received. | Iteration stops when the API signals it: `has_more: false`, an empty next cursor, a page shorter than the requested limit, or an empty page. Detection is heuristic, so it has an escape hatch. A project's [config](https://typeship.dev/docs/projects/config) can pin the style, items field, and cursor fields per operation, or turn pagination off for one. Malformed rules fall back to detection with a warning on the generation. ## Retries and timeouts * Requests with idempotent verbs (GET, HEAD, PUT, DELETE, OPTIONS) retry on `408`, `429`, `500`, `502`, `503`, and `504`, and on transport failures. * `429` is retried for every verb, including POST. * Backoff is exponential with full jitter, starting at 300ms and capped at 10 seconds per wait. A `Retry-After` header (seconds or HTTP date) is honored, capped at 60 seconds. * Defaults are two retries after the first attempt and a 30 second timeout per attempt. The policy is tunable per project in [config](https://typeship.dev/docs/projects/config): replace the retryable status set, change the retry count and backoff window, allow retries on non-idempotent operations, or disable retries globally or per operation. ### Idempotency keys When an operation declares an `Idempotency-Key` header, the SDK generates a UUID for it on every call you do not supply one for, and reuses that UUID across retries of the call. That is what makes retried writes safe. Pass your own through the params object (`idempotencyKey` in TypeScript, `idempotency_key` in Python, `IdempotencyKey` in Go) to control it. ## Hooks and debug logging Two hooks run on every attempt. `onRequest` runs after auth and body headers are assembled and may mutate `headers` or `url`. `onResponse` runs after every HTTP response, before parsing and retry decisions. `onError` runs once per failed call, after retries, with the typed error and the request method and path. ```ts const client = new AcmeClient({ bearerToken: token, onRequest: (context) => { context.headers["Trace-Id"] = crypto.randomUUID(); }, onResponse: (response, context) => { metrics.observe(context.method, response.status, context.attempt); }, onError: (error, request) => { logger.warn({ error, ...request }); }, }); ``` `debug: true` (or `ACME_DEBUG=1` in the environment) logs one line per attempt to stderr: `acme POST /accounts -> 201 (43ms) req_8f2k1`. Pass a function instead to feed your own logger a structured event with `method`, `path`, `status`, `durationMs`, `attempt`, `requestId`, and the transport error message when there was no response. Debug events never include headers or bodies, so credentials cannot reach logs through them. When you need request-level detail, the hooks are the deliberate step up. Every request carries a `User-Agent` of `acme/2.3.0 (typeship)`, so your API can tell SDK versions apart. Override it with `defaultHeaders`. The package exports a `VERSION` constant. ## Runtime validation Types catch mistakes at compile time. They cannot see an API that drifted from its spec at runtime. Turn on validation to schema-check JSON request and response bodies against your spec's own schemas, still with zero dependencies. The validator ships in the package, and the schema table is plain data generated from the spec. **TypeScript** ```ts const client = new AcmeClient({ validate: true }); const result = await client.accounts.get("acct_123"); if (!result.ok && result.error instanceof ValidationError) { result.error.violations; // [{ path: "response.balance", message: "expected integer, got string" }] } ``` Failures are never thrown. A `ValidationError` comes back as the error side of `ApiResult`. Use `validate: { mode: "warn" }` to log with `console.warn` and proceed, and `requests` / `responses` to toggle each direction. **Python** ```python client = AcmeClient(validate=True) # raises ValidationError client = AcmeClient(validate="warn") # warnings.warn and proceed ``` `ValidationError.violations` is a list of `(path, message)` pairs. `direction` is `"request"` or `"response"`. **Go** ```go client, _ := acme.New(acme.WithValidation(acme.ValidateError)) // returns *ValidationError client, _ := acme.New(acme.WithValidation(acme.ValidateWarn)) // reports through WithDebug and proceeds ``` Bad request bodies are caught before any HTTP is sent. The checks honor `readOnly`, `writeOnly`, and nullability. Constraints outside the emitted subset (`format`, `multipleOf`, and similar) are ignored, so validation can miss drift but never rejects valid traffic. All three languages validate against identical tables. GraphQL and streaming operations are not validated. ## Webhooks When your spec declares a `webhooks` section (OpenAPI 3.1, or `x-webhooks` on 3.0), the package ships typed events and a verifying parser. Verification follows the Standard Webhooks convention: `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers, HMAC-SHA256, a five minute tolerance, and a constant-time compare. ```ts const client = new AcmeClient({ webhookKey: process.env.ACME_WEBHOOK_KEY }); // in your webhook handler: const event = await client.webhooks.unwrap(rawBody, req.headers); switch (event.type) { case "account.updated": event.account; // fully typed per event case "account.closed": event.account_id; } ``` `unwrap` throws `WebhookVerificationError` on any mismatch. `unwrapUnsafe` parses without verifying. See [Webhooks](https://typeship.dev/docs/guides/webhooks) for the end-to-end flow, including local testing with your CLI. ## Streaming Operations whose success response is `text/event-stream` return a stream of events instead of a parsed body. Each event has `data`, plus `event` and `id` when the server sends them. **TypeScript** ```ts const result = await client.events.stream(); if (result.ok) { for await (const event of result.data) { event.data; // string; JSON.parse it if your API sends JSON } } ``` **Python** ```python for event in client.events.stream(): print(event.get("event"), event["data"]) ``` **Go** ```go stream, err := client.Events.Stream(ctx) if err != nil { return err } defer stream.Close() for stream.Next() { ev := stream.Event() fmt.Println(ev.Event, ev.Data) } if err := stream.Err(); err != nil { return err } ``` The per-attempt timeout guards the connection and headers only, so long streams are not cut off. Once events are flowing nothing is retried, because replaying a dropped connection would redeliver events you already handled. A failure to connect surfaces before the first event. Streaming operations are SDK-only. They are not exposed as CLI commands or MCP tools. ## Global parameters APIs where every call carries the same tenant or version parameter should not make callers repeat it. Naming those parameters in a project's [config](https://typeship.dev/docs/projects/config) promotes them to client options: set once, applied to every operation that accepts them, with per-call values winning. ```ts // generated with globals: ["account_id", "api-version"] const client = new AcmeClient({ accountId: "acct_123", apiVersion: "2026-08" }); await client.reports.list(); // both applied await client.reports.list({ accountId: "acct_9" }); // per-call value wins ``` The generated CLI and MCP server read the same values from `ACME_ACCOUNT_ID`-style environment variables. Query and header parameters are supported. Path parameters are not. ## Types ### Enums Named string enums generate a runtime value and a type with the same name, so values exist at runtime and the type stays a literal union: ```ts export const AccountStatus = { ACTIVE: "active", FROZEN: "frozen", CLOSED: "closed" } as const; export type AccountStatus = (typeof AccountStatus)[keyof typeof AccountStatus]; client.accounts.update("acct_1", { status: AccountStatus.FROZEN }); ``` Python emits `Literal["active", "frozen", "closed"]`. Go emits a defined type with `AccountStatusActive`-style constants. Inline enums stay type-only literal unions. ### readOnly and writeOnly Properties marked `readOnly` are omitted from request types, so the compiler stops you sending `id` or `created_at` fields the server owns. `writeOnly` properties are omitted from response types. In TypeScript a referenced schema appears as `Omit` in request positions. Python and Go materialize a named variant only when the filter removes something: `AccountWrite` in Python, `AccountParams` in Go. The canonical type keeps every property. ### Union types `oneOf` and `anyOf` become TypeScript unions and Python `Union[...]`. Go has no sum types, so they become a named union type holding the raw JSON with `As()`/`From()` accessors and a `Discriminator()`, never a struct that would silently drop fields. See [Go: Unions](https://typeship.dev/docs/platforms/sdk/go#unions). ### Deep bracket encoding Query objects and form-encoded bodies use bracket-style deep encoding: ```text { created: { gte: 5 } } -> created[gte]=5 { items: [{ id: "x" }] } -> items[0][id]=x { expand: ["a", "b"] } -> expand=a&expand=b (top-level arrays repeat the key) new Date(...) -> ISO 8601 string ``` JSON bodies are sent as JSON. This encoding applies to query strings and `application/x-www-form-urlencoded` bodies. ## GraphQL Clients generated from a GraphQL schema put queries on `client.query` and mutations on `client.mutation` (`client.Query` / `client.Mutation` in Go). Generated methods select all scalar and enum fields to depth 2 by default, with a fragment per concrete type for unions and interfaces, and every object-returning method takes an optional selection that replaces the default: a typed `select` object in TypeScript that narrows the result type, `select=` in Python, `WithSelection(...)` in Go. The endpoint and auth scheme come from the project's [config](https://typeship.dev/docs/projects/config#graphql-schemas), since a schema cannot declare them. Relay-style connections auto-paginate. In-band errors surface as `GraphQLRequestError` (`*GraphQLError` in Go). See [Generate from GraphQL](https://typeship.dev/docs/guides/graphql). ## Naming rules * Operations group into resources by their first tag. Untagged operations group by the first meaningful path segment. Version prefixes like `/v1` and `/api` are skipped. * A meaningful `operationId` becomes the method name. Framework suffixes such as `UsingGET` are stripped. * An `operationId` that merely restates method and path (`GetChargesCharge`) is treated as absent, and a name is derived by verb and path shape: `list`, `get`, `create`, `update`, `delete`. `GET /accounts` becomes `client.accounts.list()`. `GET /accounts/{id}` becomes `client.accounts.get(id)`. * Well-known trailing action segments win over the generic verb: `POST /charges/{id}/capture` becomes `client.charges.capture(id)`. * Resource words are trimmed from method names when the short form is unambiguous: `accounts.createAccount` becomes `accounts.create`. * Schema names that would shadow a language global or a runtime export get a `Model` suffix: a schema named `Error` becomes `ErrorModel`. * Go identifiers follow Go's initialism convention: `UserID`, `HTTPStatus`. --- # TypeScript Source: https://typeship.dev/docs/platforms/sdk/typescript.md The generated TypeScript SDK: readable source, zero runtime dependencies, ESM, and the package the CLI and MCP server are built on. The TypeScript SDK is the reference implementation of every feature on the [SDKs overview](https://typeship.dev/docs/platforms/sdk), and the package the other TypeScript platforms import. This page covers what is specific to the TypeScript package. ## Package * The npm name derives from your API's title. The Acme API produces `acme`. Set a scoped or different name per project under **package names**, for example `@acme/api`. * Zero runtime dependencies. The only entries in `package.json` are dev dependencies needed to build: `typescript` and `@types/node`. * ESM only. `"type": "module"`, an `exports` map with an `import` entry, no `require()`. Node 18 or newer, every modern bundler, browsers, and edge runtimes. * Tree-shakeable: one module per resource and `"sideEffects": false`, so bundlers drop the resources you never call. * Ships as readable TypeScript source and compiles with `npm run build`. `main`, `types`, and `exports` point into `dist/`, which holds the compiled JavaScript, `.d.ts` declarations, and declaration maps. ## What is exported ```ts import { AcmeClient, // the client environments, // named servers, when the spec declares two or more VERSION, // the package version unwrap, // ApiResult -> data, or throw the typed error NotFoundError, // one class per documented status UnexpectedApiError, TransportError, ValidationError, AccountStatus, // enums as runtime values formatDebugEvent, } from "acme"; import type { Account, AccountsListParams, AccountsListError, ApiResult, RequestOptions } from "acme"; ``` Every schema is a type. Every operation has a `Params` type when it takes parameters and a `Error` union. ## Environment variables The client reads two, and neither is a credential: | Variable | Effect | | ------------------ | ----------------------------------------------------- | | `ACME_DEBUG=1` | Turns on debug logging when `debug` is not set. | | `ACME_WEBHOOK_KEY` | The webhook signing key when `webhookKey` is not set. | Credentials are passed explicitly. That keeps the client safe in browsers and edge runtimes, where `process.env` may not exist. The generated CLI and MCP server are the pieces that read `ACME_TOKEN` and `ACME_BASE_URL`. ## Idioms * **Nothing throws on HTTP errors.** Every call returns `ApiResult` with a typed error union. `unwrap` opts into exceptions. See [Results and errors](https://typeship.dev/docs/platforms/sdk#results-and-errors). * **`PagePromise`** for list operations: `for await` walks every page, `await` returns one page. See [Pagination](https://typeship.dev/docs/platforms/sdk#pagination). * **`readOnly` becomes `Omit`.** A referenced schema in a request position appears as `Omit`. * **Enums are values.** `AccountStatus.FROZEN` exists at runtime and `AccountStatus` is also the literal-union type. * **Streams are `AsyncIterable`.** See [Streaming](https://typeship.dev/docs/platforms/sdk#streaming). * **Webhooks verify with WebCrypto**, so `unwrap` is `async` and the same code runs on Node, browsers, edge runtimes, and Workers. * **Async by nature.** Every call returns a promise; there is no separate async client because there is nothing else to be. ## Uploads A `multipart/form-data` field is typed `Blob`. Pass a `File` (a `Blob` with a name, global in browsers and Node 20+) so the part carries a filename; a bare `Blob` is sent under the field name. The other fields of the body travel alongside as form parts, nested values as JSON. A raw binary body (`application/octet-stream`, `image/png`, ...) is a `Blob` too, sent under the spec's content type. `fetch` sets the multipart boundary. ```ts await client.files.create({ file: new File([bytes], "report.pdf", { type: "application/pdf" }), purpose: "evidence" }); ``` ## Unions `oneOf` and `anyOf` become TypeScript unions of the variant types, narrowed the way you narrow any union: on a discriminator property, or with `in` checks. Nothing is wrapped and nothing is guessed at runtime; the payload is the JSON as sent. ## Response metadata Every `ApiResult`, success or failure, carries `response: { status, headers, requestId }`, so rate-limit headers, a `Location`, or a request id to quote are one property away without going through the `onResponse` hook. Error classes carry the same `response`. ```ts const result = await client.accounts.get("acct_123"); result.response.headers.get("x-ratelimit-remaining"); result.response.requestId; ``` ## GraphQL A GraphQL schema generates one method per query and mutation field on `client.query` and `client.mutation`. Arguments are the first argument and become variables. Every scalar field to depth 2 is selected by default, with a fragment per concrete type for unions and interfaces, and the optional second argument is a typed selection that replaces it and narrows the result: `client.query.account({ id }, { id: true, owner: { email: true } })`. Unions and interfaces take `on: { TypeName: { ... } }` and come back discriminated by `__typename`. A raw selection-set string is the escape hatch. Connection fields return a `PagePromise` like everything else. Errors in a `200` come back as `GraphQLRequestError` on the error side of the result, carrying the raw `errors` array. See [Generate from GraphQL](https://typeship.dev/docs/guides/graphql). ## Transport Everything rides on platform `fetch`. Pass `fetch:` to substitute your own for proxies, recording, or tests; connection reuse is whatever the runtime's `fetch` does (Node's undici and every browser pool keep-alive connections by default). Per-call `signal` composes with the per-attempt timeout, so an `AbortController` cancels a request, a paginated walk, or a stream. Streams are read incrementally through the same `fetch`. ## What else is in the package * `tests/*.test.mjs`: one smoke test per operation on `node:test` and a `node:http` mock. `npm test` builds and runs them. No framework installed, nothing contacted. * `api.md`: the complete surface reference in one file: every method, parameter table, return type, and error class. * `AGENTS.md`: context for coding agents. * With the `cli` platform, `src/cli.ts` and the `acme` bin. With the `mcp` platform, `src/mcp.ts`, the `acme-mcp` bin, `src/worker.ts`, and `wrangler.toml`. The `files` field publishes `dist/`, `src/`, and `api.md`. --- # Python Source: https://typeship.dev/docs/platforms/sdk/python.md The generated Python SDK: a typed, zero-dependency client built on the standard library, written the way Python developers expect. The Python SDK shares the [SDK](https://typeship.dev/docs/platforms/sdk) feature set: typed payloads and errors, auto-pagination, retries, hooks, validation, webhooks, and streaming. This page covers what is specific to the Python package. Python packages are SDK-only. The CLI and MCP server are TypeScript platforms. ## Package * The distribution and import name derive from your API's title. The Acme API produces `acme`, installed with `pip install .` from the package directory or from PyPI once you [publish it](https://typeship.dev/docs/guides/publish). Set a different name per project in the console under **package names**. * `pyproject.toml` declares `dependencies = []` and `requires-python >= 3.11`. The runtime is `urllib` only. * The package ships `py.typed`, so type checkers see every `TypedDict` and `Literal`. ## Idioms * **Raise, don't return.** Every failure is an exception under `TypeshipError`. See [Results and errors](https://typeship.dev/docs/platforms/sdk#results-and-errors). * **Keyword arguments for bodies.** When an operation's request body is an inline object (JSON or form-encoded), its fields become keyword arguments: `client.accounts.create(name="Ops", currency="usd")`. When the body is a named schema, an array, or plain text, it stays one `body=` argument typed accordingly (`AccountWrite`, `List[AccountWrite]`, `str`). If a body field is spelled like a path or query parameter, the whole body steps aside into `body=` rather than producing a duplicate argument. * **Dicts, typed.** Responses are the parsed JSON, typed as `TypedDict`s. You read `account["id"]`, not `account.id`. There is no conversion layer between you and the wire. * **Generators for pages.** `for account in client.accounts.list():` walks every page. `client.accounts.list_page()` returns one raw envelope. * **`request_options` per call.** A dict with `timeout`, `max_retries`, and `headers`. ## Environment variables The client reads these when the matching option is not passed: | Variable | Sets | | ------------------ | -------------- | | `ACME_TOKEN` | `bearer_token` | | `ACME_BASE_URL` | `base_url` | | `ACME_WEBHOOK_KEY` | `webhook_key` | | `ACME_DEBUG=1` | `debug` | A single API key is `api_key=` and `ACME_API_KEY`, the same spelling as the TypeScript SDK and the CLI; several keys get one keyword and variable each, named after the header or parameter. Basic auth reads `ACME_USERNAME` and `ACME_PASSWORD`. The prefix comes from your package name. ## Transport Pass `transport=` to replace `urllib`. A transport is a callable that takes `(method, url, headers, body_bytes, timeout)` and returns `(status, headers, body_bytes)`. Use it to route through a proxy, a recording layer, or a test double. Streaming endpoints read the response incrementally and go straight to `urllib`, so they bypass a custom transport. ## Webhooks `client.webhooks.unwrap(payload, headers)` verifies and parses. `unwrap_unsafe` parses only. Module-level `verify_webhook` and `sign_webhook` are exported for handlers that do not hold a client. Events are `TypedDict`s discriminated by their `type` field. `WEBHOOK_NAMES` and `WEBHOOK_SAMPLES` list every declared event with a sample payload. ## Async `AsyncAcmeClient` takes the same options and has the same methods, awaitable: `await client.accounts.get(...)`, and `async for` over paginated methods and streams. Requests run on the event loop's default executor, so nothing blocks the loop and there is still nothing to install. Both clients are context managers (`with` / `async with`) and have `close()` / `aclose()`. ## Uploads A `multipart/form-data` field is a keyword argument that accepts raw `bytes`, an open binary file (its name becomes the filename), or a `(filename, data, content_type)` tuple; the other fields of the body travel alongside as form parts. A raw binary body (`application/octet-stream`, `image/png`, ...) is `body=` with `bytes` or a file object, sent under the spec's content type. ## GraphQL A GraphQL schema generates one method per query and mutation field, keyed under `client.query` and `client.mutation`. Arguments are keyword arguments and become variables. Every scalar field to depth 2 is selected by default, with a fragment per concrete type for unions and interfaces, and `select="{ id name }"` overrides that with a raw selection set on methods that return an object. Connection fields paginate like everything else. Errors in a `200` raise `GraphQLRequestError` (an `ApiError`) with the raw `errors` list. ## Connections The default transport pools keep-alive connections per client on `http.client`, one per scheme/host/port, honoring `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`. A request that fails on a reused connection before any response arrived is retried once on a fresh one (the server closed an idle keep-alive, the same case Go's `net/http` retries), so pooling never turns into spurious transport errors. Redirects follow the rules `urllib` applied. --- # Go Source: https://typeship.dev/docs/platforms/sdk/go.md The generated Go SDK: context-first methods, (T, error) returns, typed error structs, and gofmt-clean output on net/http alone. The Go SDK shares the [SDK](https://typeship.dev/docs/platforms/sdk) feature set: typed payloads and errors, auto-pagination, retries, hooks, validation, webhooks, and streaming. This page covers what is specific to the Go module. Go packages are SDK-only. The CLI and MCP server are TypeScript platforms. ## Module * The module path comes from the project's Go destination repository: `github.com/acme/acme-go`. Configure it in the console, or override it per project under **package names**. Until a repository is set, the module path is a placeholder that `go get` cannot resolve. * The package name is read off the module path the way the Go ecosystem reads it. `github.com/stripe/stripe-go` is `package stripe`. A major-version suffix is ignored. * `go.mod` has no `require` block. The runtime is `net/http` and the standard library. * Every file passes `gofmt -l`. Every generation is gated on `go build`, `go vet`, and `gofmt`. ## Idioms * **Context first.** Every method takes a `context.Context`. Cancel it to abort a request, a paginated iteration, or a stream. * **`(T, error)` returns.** Methods with no response body return `error` alone. * **Params by pointer.** Query and header parameters, and the fields of an inline object body, live in one `XxxParams` struct passed by pointer. `nil` means no parameters. Optional fields are pointers so absent and zero stay distinct; `acme.Ptr(v)` makes one inline. * **Named bodies are arguments.** A request body that is a `$ref` to a named schema, an array, or plain text is a positional `body` argument with its own type (`AccountParams`, `[]AccountParams`, `string`), so a required body is enforced by the compiler. * **Functional options.** `acme.New(acme.WithBearerToken(...), acme.WithTimeout(...))` for the client, `acme.WithRequestTimeout(...)` and friends per call. * **Iterators.** `it := client.Accounts.List(ctx, nil)`, then `for it.Next() { it.Value() }` and `it.Err()`. * **Initialisms.** Identifiers follow Go's convention: `ID`, `AccountID`, `URL`, `HTTPStatus`. ## Environment variables `New` reads these before options apply, so options always win: | Variable | Sets | | --------------------------------- | --------------------------------------- | | `ACME_TOKEN` | bearer token | | `ACME_API_KEY` | the API key, when the spec declares one | | `ACME_USERNAME` / `ACME_PASSWORD` | basic auth | | `ACME_BASE_URL` | base URL | | `ACME_WEBHOOK_KEY` | webhook key | The prefix comes from your package name. ## Errors `APIError` carries `Status`, `Body []byte`, `RequestID`, and `Message`, plus a `Decode(v any) error` helper for the body. Per-status types such as `*NotFoundError` embed `APIError`, so `errors.As` matches at either precision. An undocumented status returns a bare `*APIError`. No response at all returns `*TransportError` with `Method`, `URL`, and a wrapped `Err`. A cancelled context returns `ctx.Err()`. ## Webhooks `client.Webhooks.Unwrap(payload, header)` verifies and returns a `*WebhookEvent` carrying `Type` and the raw `Data`. Go has no sum types, so each declared event gets an accessor: `event.AsAccountUpdated()` returns the typed payload or an error if the type does not match. Events your module has not seen yet still arrive intact. `VerifyWebhook` and `SignWebhook` are exported for handlers that do not hold a client. ## Streaming `client.Events.Stream(ctx)` returns a `*Stream`. Read it like `bufio.Scanner`: `Next`, `Event` (an `SSEEvent`), `Err`, and `Close`. The stream uses a copy of the HTTP client with no timeout, so cancel through the context. ## Uploads A `multipart/form-data` field is an `acme.Upload{Name, ContentType, Reader}` on the params struct: the name and content type the server sees, and an `io.Reader` for the bytes. The other fields travel alongside as form parts. A raw binary body (`application/octet-stream`, `image/png`, ...) is a positional `body io.Reader`, sent under the spec's content type. ## Unions Go has no sum types. A `oneOf`/`anyOf` becomes a named union type that holds the JSON as received and decodes on request: `As() (Variant, error)` and `From(v)` per variant, `Raw()` for the bytes, and `Discriminator()` when the spec names one. It is the same shape oapi-codegen generates, and the same accessor pattern the webhook events use. A struct that pretended to be a union would silently drop fields; `any` would drop the typing. ## Response metadata `WithAPIResponse(&meta)` on any call fills an `APIResponse{StatusCode, Header, RequestID}` from the last HTTP response (rate-limit headers, a `Location`, a request id to quote) without going through the global `WithOnResponse` hook. ## GraphQL A GraphQL schema generates one method per query and mutation field on `client.Query` and `client.Mutation`. Arguments are the params struct and become variables. Every scalar field to depth 2 is selected by default, with a fragment per concrete type for unions and interfaces, and `WithSelection("{ id name }")` overrides that per call on methods that return an object. Connection fields return an `*Iter[T]` like everything else. Errors in a `200` return `*GraphQLError` (which unwraps to `*APIError`) carrying the raw `Errors`. --- # CLI Source: https://typeship.dev/docs/platforms/cli.md Every operation in your API becomes a command with typed flags, JSON output, and exit codes, plus login, config, docs, completion, and webhook tooling including the hosted relay. The `cli` platform adds a command-line interface to your TypeScript package. Every operation becomes a command that calls the generated SDK. API commands take flags only, print JSON, and exit with a code, so they run in CI and in scripts without wrappers. The bin is named after your API, so the Acme API ships `acme`. ```bash acme [args] [--flags] acme accounts list --limit 50 acme accounts get acct_123 acme accounts create --name "Operating" --currency usd ``` Path parameters are positional arguments in path order. Everything else is a flag. Subcommands are trimmed of the resource word, so `accounts list` rather than `accounts list-accounts`. The full name stays routable, so nothing you script breaks when the short form appears. Command names come from the spec's operationIds when they say something the path does not, and are derived otherwise: `list`, `get`, `create`, `update`, `delete`, or the action segment (`charges capture`, `invoices mark-uncollectible`). One item of a sub-collection is singular and the collection plural (`customers create-card`, `customers list-cards`, `customers get-card `), a singleton is `get`/`update` (`customers get-cash-balance`), and two actions with the same word under one resource are told apart by their parent (`billing deactivate-meters`, `billing deactivate-alerts`), never by a number. An operationId's `ById`/`ByName` tail that just names the positional argument is dropped, so `getPetById` is `pet get `. A resource whose name is one of the CLI's own words (`config`, `docs`, `mcp`, `webhooks`, `login`, and the rest listed below) is exposed as `-api`, so `acme config` is always the CLI's config and `acme config-api list` is your API's. Generation reports the rename. ## Flags from the spec * Body fields, query params, and header params become typed flags named after their wire names: `--name`, `--currency`, `--limit`, `--idempotency-key`, `--cursor`. * A parameter whose name matches a flag the CLI reads on every API command (`data`, `all`, `select`, `base-url`, `debug`, `validate`, `non-interactive`, `color`, `version`, `help`, and the auth flags such as `token` or `api-key`) is prefixed with where it goes: a body field named `data` is `--body-data`, so `--data` is always the raw body. Two parameters sharing a name across path, query, and body get the same treatment. * Boolean flags stand alone or take `true`/`false`: `--active`, `--active false`. * Numbers and booleans are validated. `--limit abc` is a usage error. Enum values are checked against the spec's allowed list, for scalars and for array elements. * Array fields take a comma list, the flag repeated, or a JSON array, and a single value is always sent as a one-item array: `--tags a,b`, `--tags a --tags b`, `--tags '["a","b"]'`, and `--platforms cli` sends `["cli"]`. Help shows the element type (`string[]`, `available|pending|sold[]`, `object[]`). * Object fields take JSON: `--metadata '{"team":"core"}'`. Help shows them as `object`; a value that is not a JSON object is a usage error. * Loosely typed fields (unions, untyped values) show as `json` and accept JSON; a value that is not valid JSON passes through as a plain string. * `--data ''` supplies a raw JSON body. When the operation also has field flags, `--data` is the base object and field flags are merged on top. Flags win. `--data @body.json` reads the body from a file and `--data -` reads it from stdin, the curl conventions; `--file -` does the same for a binary body. * For CLIs generated from a GraphQL schema, object-returning operations accept `--select '{ id name }'`, a raw selection set that replaces the default depth-2 selection. Help and `docs` show each command as its root field (`GraphQL query account`), since every call is one POST to the endpoint. ```bash acme accounts create \ --data '{"name":"Operating","currency":"usd"}' \ --name "Ops" # body sent: { "name": "Ops", "currency": "usd" } ``` Mistyped names get a suggestion. `acme accountz list` answers `Unknown command: accountz. Did you mean 'acme accounts'?` and exits 2. The same applies to subcommands and flags. Flags for `date` and `date-time` parameters take relative forms as well as ISO 8601: `--created-after -7d` (or `-P7D`, `"7 days ago"`, `today`, `now`) resolves against the clock before the request, so the API sees an absolute value. An unsigned duration (`7d`) is a usage error that names the signed forms. The help table shows `date-time` as the type and names the forms. ## Output and exit codes Success prints the JSON response on stdout, pretty-printed. Paginated commands print one page plus what fetches the next one: `{ "items": [...], "hasMore": true, "nextPage": { "limit": 2, "cursor": "cur_2" }, "nextCommand": "acme accounts list --limit 2 --cursor cur_2" }`. `nextPage` is the same shape the MCP tool returns; `nextCommand` is ready to run. `--fields id,name,owner.email` keeps only those paths of the result (per item for pages and `--all` streams), which is most of what `jq` gets used for and a token saver for agents. Every failure is one JSON envelope on stderr whenever stderr is not a terminal (pipes, CI, agents): ```json { "status": "error", "issues": [{ "code": "NOT_FOUND", "message": "no such account" }], "docs_url": "https://docs.acme.example", "next_steps": ["Check the id; list the resource first."], "detail": { "status": 404, "error": "NotFoundError", "body": { "code": "not_found", "message": "no such account" } } } ``` `status` is `error` when the command failed and `action_required` when it stopped on purpose and `next_steps` says what unblocks it (a missing credential, a plan limit, a confirmation). `issues[].code` is stable; branch on it, never on the message. The message is the API's own words when it sent any; `detail` carries the API's body, the SDK error class, and the request id, so nothing is lost. A person at a terminal gets the same envelope as prose: the message, the next steps, and a dim trailer with the code, the HTTP status, the exit code, and how to get the JSON: ```text $ acme accounts get acc_404 acme: no such account Check the id; list the resource first. NOT_FOUND · HTTP 404 · exit 1 · pipe stderr or --mode agent for JSON ``` `--mode human` forces the prose form and `--mode agent` (or `--format json`) the envelope, whatever stderr is. | Code | Exit | When | | ---------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------ | | `NO_AUTH` | 1 | 401 with no credential resolved. `action_required`. | | `AUTH_INVALID` | 1 | 401 or 403 with a credential sent. | | `PLAN_LIMIT` | 1 | 402. `action_required`; `next_steps` carries the upgrade URL when the API sends one. | | `NOT_FOUND` | 1 | 404. | | `INVALID_REQUEST` | 1 | 400, 409, 413, 422: the API rejected the input. | | `SPEC_INVALID` | 1 | 422 with typeship's `spec_error` (the typeship CLI). | | `RATE_LIMITED` | 1 | 429. `action_required`; `next_steps` says how long to wait. | | `SERVER_ERROR` | 1 | 5xx. | | `NETWORK_ERROR` | 1 | No response: DNS, TLS, refused, timed out. | | `VALIDATION_FAILED` | 1 | `--validate` found the body does not match the schema; `detail.violations`. | | `CONFIRMATION_REQUIRED` | 2 | A destructive command needs `--force`. `action_required` with the exact command. | | `TTY_REQUIRED` | 2 | A prompt was needed and there is no terminal. | | `INVALID_USAGE`, `UNKNOWN_COMMAND`, `UNKNOWN_FLAG`, `MISSING_ARGUMENT` | 2 | Wrong flags or arguments. | Transport errors still name the request and the deepest cause in the message: `TransportError: GET https://... failed: fetch failed: connect ECONNREFUSED ...`. Bare invocations (`acme`, `acme accounts`) print help on stderr and exit 2, so a misassembled command in a script still fails and never pollutes stdout. `acme --help`, `acme accounts --help`, `-h`, and the bare word `help` print to stdout and exit 0. When the spec says an operation is authenticated (`security` at the operation or document level, without an anonymous alternative) and no credential resolves from flags, environment, or `login`, the command stops before sending anything: `NO_AUTH`, `action_required`, `next_steps` naming the env var and `login`. Operations with `security: []` run without one and show `(no auth needed)` in help; `[{}, {...}]` shows `(auth optional)`. A 401 from the API with a credential sent is `AUTH_INVALID`, as before. ## Streaming every page Paginated commands accept `--all`, which walks every page and streams one item per line (NDJSON) on stdout: ```bash acme accounts list --all > accounts.ndjson acme accounts list --all | jq -r '.id' ``` ## Help at every level ```bash acme # resources and their commands acme accounts # commands for one resource acme accounts create --help # flags, types, required markers, an example ``` Command help is a table sized to its content: the flag, its type (`string`, `number`, `string[]`, `usd|eur`, `object`), a required marker, and the first sentence of the spec's description wrapped to the terminal width; enum lists too long for the type column go on their own line. Pagination and idempotency parameters the engine recognized get a description even when the spec left them blank. Every command ends with an example built from its required inputs. `acme docs accounts create` has the untruncated descriptions. Root help lists each resource's commands in CRUD order (`list, get, create, update, delete`, then the rest as the spec orders them), wrapped to the terminal; an API with more than 120 operations gets a digest per resource (`list, get, create, … 24 more (acme accounts)`) and the resource's own help has the full list. Root help also lists the auth environment variables, the credentials path, and the setup commands. ## Auth and configuration Credentials come from environment variables named after your package, with flag overrides. For the `acme` package the prefix is `ACME`: | Environment variable | Flag | Used when the spec declares | | --------------------------------- | --------------------------- | ----------------------------------------- | | `ACME_TOKEN` | `--token` | HTTP bearer auth, or OAuth2 access tokens | | `ACME_API_KEY` | `--api-key` | A single API key header | | `ACME_USERNAME` / `ACME_PASSWORD` | `--username` / `--password` | HTTP basic auth | | `ACME_BASE_URL` | `--base-url` | Always available | With several API key schemes, each key gets its own variable and flag named after the header or query parameter. Precedence at request time is flags, then environment variables, then credentials saved by `login`. The base URL resolves as `--base-url`, then `ACME_BASE_URL`, then the stored `base-url`, then the stored `environment`, then the spec's default server. If none exists the command exits 2 with `No base URL. Pass --base-url, set ACME_BASE_URL, or run 'acme config set base-url '.` ## Version `--version`, `-v`, or the bare word `version` prints the package version, the API version it was generated from, and the generator: ```text $ acme --version acme 2.3.0 (Acme API 2.3.0, generated by typeship) ``` ## login, logout, whoami Instead of exporting variables every session, credentials can be stored once. `login` saves them to `~/.config/acme/credentials.json` with mode 600, honoring `XDG_CONFIG_HOME`. `logout` deletes the file. ```bash acme login # hidden prompt on a TTY acme login --token # any auth flag works cat token.txt | acme login --with-token # stdin, for scripts ``` When the API offers browser approval (an endpoint pair the project configures; typeship's own API does), `acme login` with no flags opens a page where the person approves, and the API mints a key for this machine that `login` stores. Nothing is pasted. Under an agent, or with `--no-browser`, the CLI prints the approval URL (as a JSON event on stderr too) and polls until the person decides; the key never crosses the conversation. The request is PKCE-shaped: the CLI keeps a verifier and the API hands the key only to the poll that proves it. `login` is otherwise the one command that prompts, and only on a TTY. When the spec declares OAuth2 and the CLI knows a client id (set in the console, passed with `--client-id`, or in `ACME_CLIENT_ID`), `login` runs the RFC 8628 device flow. It prints a code and a URL, polls until you approve in the browser, and refreshes the token automatically a minute before it expires. Scopes and an audience can be configured in the console for authorization servers that gate refresh tokens behind `offline_access` or need an audience for API-valid tokens. `whoami` calls your API's identity endpoint with whatever credentials would be used, so it answers "which account is this machine acting as". Generation detects endpoints like `GET /me` automatically. Project settings can pin a specific `resource.method`. If the API has no such endpoint, `whoami` reports where credentials would come from without a network call, and exits 1 when there are none. ## config Stored defaults live next to credentials in `~/.config/acme/config.json`, so the base URL does not need repeating on every command. When the spec declares several servers, environments can be pinned by name: ```bash acme config set base-url https://api.staging.example.com acme config set environment sandbox # names come from the spec's servers acme config set docs-url https://docs.acme.example acme config list acme config get base-url acme config unset base-url acme config path ``` Flags always win over stored config, so CI overrides need no cleanup. ## mcp When the package was generated with the `mcp` platform, the CLI wires the server into agent clients: ```bash acme mcp # print the server entry as JSON, and which clients were detected acme mcp install --all # write it into every agent client found on this machine acme mcp install --claude # Claude Code (./.mcp.json) acme mcp install --codex # Codex CLI (~/.codex/config.toml) acme mcp install --vscode # VS Code (./.vscode/mcp.json) acme mcp install --windsurf | --gemini | --opencode | --zed | --claude-desktop acme mcp --url https://typeship.dev/mcp/ --claude # a remote endpoint instead acme mcp install --claude --read-only # a server that cannot write ``` `--read-only` registers the local server with its `--read-only` flag, or points a hosted entry at `/readonly`; either way writes are not callable. Existing entries in the client config are preserved. The entry is the API's hosted MCP endpoint when the project has one (with the auth env var written as a reference, `${ACME_TOKEN}`, never a literal key), else the package's local stdio server, which reads the credentials `login` saves. `--all` skips Cursor until Cursor speaks MCP 2026-07-28; `--cursor` writes it on request with a note. See [MCP server](https://typeship.dev/docs/platforms/mcp). ## init One command connects a machine, or a repository, to the API: ```bash acme init --all -k ``` It stores the credential (or notes the env var already set), installs the API's skills when a repository is configured (`npx skills add owner/skills`), writes the MCP entry into every detected agent client, and upserts a marked `acme` block into `./AGENTS.md` (or `CLAUDE.md` when only that exists, or when running under Claude Code) with the auth rules, the discovery commands, and a compact index of every command. Idempotent: run it again and the block is replaced in place. `--no-skills`, `--no-mcp`, `--no-agents-md` skip a part; the report on stdout says what happened to each. ## agent-guide, auth check, doctor, help --json For the agent holding only the binary: ```bash acme agent-guide --format json # conventions, first command, docs index, hosted MCP, skills, next steps acme help --json # every resource, command, positional, and flag, as data acme auth check [--live] # which credential the CLI would send (and, with --live, whether the API accepts it) acme doctor # node, credentials, base URL, docs, MCP client config, with next_steps ``` All four print JSON. `auth check` and `doctor` exit 1 when something is `action_required`. ## webhooks listen _Available on Pro and Enterprise._ Local webhook development without tunnels. Developers integrating your webhooks need events to reach a handler on their laptop. Tunnels work, but they expose a port, need an account somewhere, and drop events while disconnected. Turn on **webhook relay** under the CLI platform in [project settings](https://typeship.dev/docs/projects#platforms), which needs the CLI on and Pro, and the next generation bakes the relay coordinates into your CLI, which gains a `webhooks listen` command that needs none of that: ```text $ acme webhooks listen --forward-to localhost:3000/webhooks Ready! Forwarding relayed events to http://localhost:3000/webhooks Point this API's webhook endpoint at: https://typeship.dev/relay/rls_... (^C to quit) 200 account.created [1] (43ms) ``` 1. `listen` mints a session with its own unguessable ingest URL. Two developers on the same project never see each other's events. 2. The developer registers that URL as a webhook endpoint in your API, the same way they would register any endpoint. 3. Events sent there are buffered and replayed to the local handler with their **original headers and body**. Nothing is re-signed, so signature verification with the generated SDK's `webhooks.unwrap` works unchanged. 4. `--events account.created,account.closed` filters by the payload's type field. If the connection drops, events keep buffering and replay on reconnect within the same run. Relayed payloads are transient. Events are deleted 24 hours after they arrive, and sessions are removed after 48 hours idle. Bodies are capped at 1MB, sessions at 5,000 events, and the ingest URL is rate limited per IP. Pair it with `webhooks fake`, below, to cover the whole local loop before any real event exists. ## webhooks fake When the spec declares webhooks, `webhooks fake ` sends (or prints) a signed sample event with Standard Webhooks headers, so handlers are testable before any real event exists: ```bash acme webhooks fake # list the declared events acme webhooks fake account.updated # print headers and payload acme webhooks fake account.updated --forward-to localhost:3000/webhooks ``` The signing key is `--key`, then `ACME_WEBHOOK_KEY`, then a throwaway key. See [Webhooks](https://typeship.dev/docs/guides/webhooks). ## docs The reference and the guides, in the terminal. `docs` alone prints the API overview. `docs ` is the full operation reference with untruncated descriptions, richer than `--help`. ```bash acme docs accounts create acme docs search "idempotency" acme docs read quickstart acme docs --web ``` Guides come from your documentation site through the `llms.txt` convention, which most docs hosts publish automatically. Set the site in the console, or with `config set docs-url`. Your spec's `externalDocs` URL is the default. `search` covers both the reference and the guides. `read` prints a guide page. Pages cache locally for an hour, and nothing is fetched unless a docs command runs. The same two layers back the MCP server's `search_docs` and `read_docs` tools. ## completion Tab completion for resources, commands, every flag, and the values a flag takes (enum values, `true`/`false`, `--color on|off|auto`), plus `config` keys and `mcp install` targets, generated from the same operation table as the CLI: ```bash # ~/.bashrc eval "$(acme completion bash)" # ~/.zshrc eval "$(acme completion zsh)" # fish acme completion fish > ~/.config/fish/completions/acme.fish ``` ## upgrade When the package is published to an npm registry, the CLI can update itself. `upgrade` compares the running version against the registry's latest and installs it with `npm install -g`. `upgrade --check` reports without installing. A custom registry is honored through `npm_config_registry`. If the package is not published, `upgrade` says so. A binary that was installed with pnpm, bun, or yarn, or is running through `npx`, is not upgraded in place: `upgrade` names the right command for that manager instead. Vendored packages update by regeneration, not registries. By default the CLI never contacts a registry on its own. You can opt into an update notice in the console. Then a once-a-day check caches the latest version and later runs print a one-line hint on stderr. The check has a 1.5 second timeout, and `--non-interactive` silences it. ## feedback When you configure a support URL in the console, `feedback` opens it in a browser. GitHub issue URLs arrive with the CLI and API version, Node version, and platform prefilled in the body, visible before anything is submitted. Nothing is sent on its own. ## Color Help text and stderr hints use light color on a TTY. JSON output is never colored, so parsers and agents see identical bytes either way. `--color on|off|auto` overrides detection, and `NO_COLOR` is honored. An explicit `--color on` wins over everything, for terminals that hide their TTY. ## Debug output `--debug` (or `ACME_DEBUG=1`) prints one line per HTTP attempt to stderr: method, path, status, duration, request id. stdout stays pure JSON, so piping is unaffected. Debug lines never include headers or bodies. ## Runtime validation `--validate` schema-checks request and response bodies against the spec with the SDK's zero-dependency validator. Violations exit 1 with a `ValidationError` payload listing each JSON path. It is the fastest way to see whether an API has drifted from its published spec. ## Agent mode The CLI knows when an agent is driving it: `--mode agent`, `ACME_MODE=agent`, or no terminal on stdin and stdout. In agent mode nothing prompts and no browser opens (`docs --web` and `feedback` print the URL instead), and every stop is an `action_required` envelope with `next_steps` the agent can follow. `--mode human` forces the interactive behavior back on. Destructive commands (every `DELETE`) need `--force` (or `--yes`, `-y`, or `ACME_YES=1`). A person at a terminal is asked; an agent gets `CONFIRMATION_REQUIRED` with the exact command to run once the user has agreed. Nothing is deleted on a guess. Every request carries a `User-Agent` of `-cli/ (typeship; harness=; agent)`: the harness when one is detected (Claude Code, Cursor, Codex, Gemini CLI, and others, by their environment variables), `agent` when a harness is detected or `--mode agent` is set, and `non-interactive` for a bare run without a terminal, so a CI job is not counted as an agent. No secrets ride in it. `--out ` writes a file-shaped response (an array of `{path, content}` objects) into a directory and prints the rest of the response with a summary, for APIs that return generated or exported files. ## Non-interactive mode `--non-interactive` (or `ACME_NON_INTERACTIVE=1`) is the explicit switch: `login` skips the device flow and the prompt and fails fast with `TTY_REQUIRED` unless the credential arrives through a flag or `--with-token`, and the update notice stays silent. Agent mode implies everything non-interactive mode does except that `login` still runs the device flow, printing the URL and code as JSON for the agent to relay. ## In CI Credentials come from the environment, output is JSON, and failures are exit codes. Build the package in the job, then run the compiled bin with a secret: ```yaml title=".github/workflows/report.yml (step excerpt)" - name: Build the SDK package working-directory: vendor/acme run: npm install && npm run build - name: Export account ids env: ACME_TOKEN: ${{ secrets.ACME_TOKEN }} run: | node vendor/acme/dist/cli.js accounts list --all \ | jq -r '.id' > account-ids.txt ``` ## Uploads and event streams Operations with multipart bodies take their binary fields as file paths: `acme uploads avatar --file ./me.png --label "me"` reads the file and sends it as a form part with the other fields alongside. An operation whose whole body is binary takes `--file ` for the body. Operations that stream server-sent events print one JSON line per event (`{ "event", "id", "data" }`) until the stream ends, the same shape `--all` uses for pages, so `acme events stream | jq` works. These operations are SDK and CLI only; the MCP server does not expose them as tools. --- # MCP server Source: https://typeship.dev/docs/platforms/mcp.md Every operation in your API becomes a tool that coding agents can call. Zero dependencies, two transports, one login shared with your CLI, and a hosted endpoint at a stable URL. The `mcp` platform adds a Model Context Protocol server to your TypeScript package. It exposes each operation as a typed tool, ships a `search_docs` and `read_docs` pair so agents can read your reference and guides, and shares credentials with your CLI. It has no runtime dependencies. The bin is named after your API, so the Acme API ships `acme-mcp`. The server speaks the current MCP revision, `2026-07-28`: stateless, one request at a time, no `initialize` handshake and no sessions. That is the shape the protocol settled on, and it is the shape a generated API server wants anyway. Claude Code connects to it out of the box; see [Protocol notes](#protocol-notes) for what other clients need. The server reaches agents two ways: inside your package, as a process the client spawns or a Worker you deploy, or as the [hosted endpoint](#hosted-endpoint) typeship runs for you at a stable URL. To connect Claude Code, Cursor, or Claude Desktop, see [Connect MCP clients](https://typeship.dev/docs/guides/mcp-clients). ## Transports The same compiled server speaks two transports. The transport decides where credentials come from. ### stdio `node dist/mcp.js` speaks newline-delimited JSON-RPC 2.0 over stdin and stdout. This is the transport local MCP clients spawn themselves. Build the package once first: ```bash cd acme npm install npm run build # clients run dist/mcp.js ``` Credentials and the base URL resolve exactly as they do for the CLI. Environment variables win, then whatever `acme login` and `acme config` saved: | Setting | Environment variable | Saved by | | ------------ | ------------------------------------------ | ----------------------------- | | Bearer token | `ACME_TOKEN` | `acme login` | | API key | `ACME_API_KEY`, or one variable per header | `acme login` | | Basic auth | `ACME_USERNAME` / `ACME_PASSWORD` | `acme login` | | Base URL | `ACME_BASE_URL` | `acme config set base-url` | | Environment | | `acme config set environment` | One `login` covers the CLI and every agent client. No secrets need to live in client config files. If no base URL can be resolved, the first tool call returns an `isError` result naming the fix (`set ACME_BASE_URL` or `acme config set base-url`); the server stays up. The package also declares the server as a bin, so `npx acme-mcp` (or `acme-mcp` after a global install) starts it the same way `node dist/mcp.js` does. Nothing but JSON-RPC is written to stdout. Startup errors and `ACME_DEBUG=1` request lines go to stderr. ### Streamable HTTP `node dist/mcp.js --http 3000` serves the same tools over HTTP. The port falls back to `PORT`, then 3000. The module also exports `handleHttp`, a fetch-style handler (`Request` in, `Response` out) you can mount on any route or edge runtime. Requests without an `Origin` header (every non-browser MCP client) are always accepted. When `Origin` is present the server validates it, as the spec requires against DNS rebinding: same-host and localhost origins are allowed, anything else is `403` unless listed in `ACME_MCP_ALLOWED_ORIGINS` (comma-separated; `*` allows any). CORS headers reflect the allowed origin. In HTTP mode, each request's `Authorization` header is forwarded to your API, so every caller uses their own credentials. Environment variables still work for credentials the server itself should hold. The HTTP server is stateless, as the `2026-07-28` transport is by design. It accepts one JSON-RPC message per POST, never mints or reads session ids, and does not open a server-to-client stream. Notifications return `202`. Anything other than POST returns `405`. Every request must carry the transport's mirrored headers, and the server checks them against the body as the spec requires: `MCP-Protocol-Version` (must equal the version in `_meta`), `Mcp-Method` (must equal `method`), and `Mcp-Name` on `tools/call` (must equal `params.name`; the `=?base64?…?=` encoding is decoded first). A missing or mismatched header is `400` with JSON-RPC error `-32020`. An unknown method is `404` with `-32601`. A JSON-RPC batch or a malformed message is `400` with `-32600`. ## The `acme mcp` command The fastest way to wire the server into an agent client is the CLI's own `mcp` command: ```bash acme mcp # print the server entry as JSON acme mcp --claude # write ./.mcp.json (Claude Code) acme mcp --cursor # write ./.cursor/mcp.json acme mcp --claude-desktop # write the Claude Desktop config acme mcp --url https://typeship.dev/mcp/ --claude # a remote endpoint instead ``` Local entries are `{ "command": "node", "args": ["/abs/path/acme/dist/mcp.js"] }`. Remote entries are `{ "type": "http", "url": "..." }`. Existing servers in the target file are preserved. Only the `acme` entry is added or replaced. Claude Desktop's config file only launches local servers, so `--claude-desktop` with `--url` stops with the instruction to add the URL as a connector in the app instead. The written entry has no `env` block. The server reads the credentials that `acme login` saved, so tokens never sit in a config file checked into a repo. ## Tools One tool per operation, named `resource_method` in lower snake case: `accounts_list`, `accounts_get`, `spec_versions_get_content`. GraphQL root fields follow the same shape: `query_account`, `mutation_create_account`. Names are limited to lowercase letters, digits, and underscores, capped at 60 characters, and deduplicated. * Descriptions give agents the operation summary, the first sentence of its description when that adds something, and the method and path as ground truth: `Create an account. Accounts hold a balance in one currency. POST /accounts`. Paginated tools add `(paginated: returns one page plus hasMore and nextPage arguments)`. The full description stays available through `read_docs`. * Guardrails the spec already states are folded in, so the agent reads them before the call instead of in the error. A deprecated operation's description leads with `Deprecated.`; an operation whose `security` names OAuth scopes ends with `Requires scope accounts:write.`; an operation that works without credentials in an API that otherwise has them says `No credential needed.` Argument descriptions gain enum meanings from `x-enumDescriptions` (or `x-enum-descriptions` / `x-enum-varnames`) as `Values: active (open and usable), frozen (temporarily locked)`, the schema `default`, `Markdown; use literal newlines.` for `text/markdown` content, `Deprecated.` for deprecated parameters, and, for a `_id` argument whose thing has exactly one list operation, `IDs come from things_list.` The same notes appear in `read_docs` and CLI help. * When the spec cannot say it (a three-step upload, a slow endpoint), write the tool description yourself: `mcp.tool_descriptions` in the project's [config](https://typeship.dev/docs/projects/config), keyed by operationId or `"METHOD /path"`, replaces the derived text for that operation in the package and on the hosted endpoint. Keys that match nothing are generation warnings. * Each tool carries a `title` (the operation summary) and spec annotations: `readOnlyHint` for GET and HEAD, for GraphQL queries, and for POST operations the spec names as reads (`search`, `query`, `find`, `count`); `destructiveHint` for DELETE, PUT and PATCH (they remove or overwrite) and for operations named like `cancel`, `archive`, `revoke`, while POST creates are additive; `idempotentHint` for reads, PUT and DELETE; `openWorldHint` false, since the server talks to one known API. Clients such as Claude Code use these to auto-approve reads and confirm destructive writes. * Input schemas are JSON Schema 2020-12 derived from your spec: real types, enums, formats, descriptions, defaults, and required fields for path, query, header, and body parameters. `allOf` compositions are merged into one flat argument list, `readOnly` properties are left out of inputs, and nullable fields keep their nullability. Nested schemas are inlined to a fixed depth so models can read them without resolving references. * Tools carry an `outputSchema` when your spec documents a success body (paginated tools describe `items`, `hasMore`, `nextPage`), and successful results carry the JSON as `structuredContent` alongside the text block. Error results keep their JSON in the text only, since `structuredContent` must match the schema. * Operations whose body is a plain object take its fields as top-level arguments. Other body shapes take a single `body` argument. Argument names are your wire names. * Every tool also takes `fields`, an array of dotted paths that keeps only those keys of the result (`["id", "name", "owner.email"]`), applied per item on paginated tools. It is the same projection the CLI's `--fields` does, and the way to keep large responses small. An operation that already has a `fields` parameter keeps its own. * GraphQL operations that return an object accept a `select` argument, a raw selection set such as `{ id name }`, that replaces the default depth-2 selection. It is declared in the tool's input schema with the default spelled out. Every server also ships two documentation tools: * `search_docs` searches your reference (operations, parameters) and, when a docs site is configured, your guides. Reference matches are ranked (tool name first, then summary, path and argument names, then description) and paged 15 at a time through `page`. * `read_docs` returns an operation's full reference by tool name or dotted `resource.method`, or a guide page from your docs site. The reference comes from the spec and is baked into the package. Guides come from your documentation site through the `llms.txt` convention. See [Docs for agents](#docs-for-agents). ### Tool mode for large APIs Hundreds of per-operation tools flood an agent's context. The `meta` tool mode collapses the server to exactly three tools: * `search_docs` and `read_docs`, as above. * `execute`, which runs any operation by name with a JSON `arguments` object. It accepts tool names (`accounts_create`) or dotted names (`accounts.create`). Set it with `mcp.tool_mode` in the project's [config](https://typeship.dev/docs/projects/config): `auto`, `operations`, or `meta`. `auto` is the default and switches to `meta` above 100 operations. The hosted endpoint honors the same setting. ### Arguments are checked, never dropped Before anything reaches your API, the arguments of a tool call are checked against the tool's input schema, and every problem comes back at once as one `isError` result, so a single round trip fixes the call: * An unknown argument name is an error with a suggestion (`"nam": did you mean "name"?`). When only case or punctuation differs (`accountId` for `account_id`), the value is matched to the accepted name and the call proceeds. * Values are coerced where the intent is clear (`"true"` to a boolean, `"3"` to an integer, a JSON string to an object, a lone scalar to a one-item array, `US` to the enum member `us`) and rejected where it is not (`"abc"` for an integer), naming what was expected. * Missing required arguments are listed with their descriptions. * Date and date-time arguments take relative forms as well as ISO 8601: `-P7D` and `-7d` (seven days ago), `+PT1H` and `+2h` (ahead), `7 days ago`, `in 2 weeks`, `today`, `yesterday`, `now`. They resolve against the server's clock before the request, so your API sees an absolute value (`YYYY-MM-DD` for `date`, an ISO instant for `date-time`). An unsigned duration (`7d`) is rejected as ambiguous, naming the signed forms. The argument's description says which forms it takes. ```json { "error": "InvalidArguments", "code": "INVALID_ARGUMENTS", "message": "2 problems with the arguments to accounts_create; nothing was sent to the API.", "issues": [ { "code": "UNKNOWN_ARGUMENT", "argument": "nam", "message": "Unknown argument \"nam\"; did you mean \"name\"? Accepted: name, currency, tags, regions, metadata, fields." }, { "code": "MISSING_ARGUMENT", "argument": "name", "message": "Missing required argument \"name\"." } ], "next_steps": ["Fix the arguments listed in issues and call accounts_create again.", "read_docs {\"page\": \"accounts_create\"} lists every argument with its type and whether it is required."] } ``` Nothing is dropped silently: an argument your API would not understand never becomes a request that fails for an unclear reason, or worse, a request that succeeds without the model's intent. ## Results and errors Tool results are compact JSON text content. Paginated tools return one page plus a continuation signal and, when there is another page, the exact arguments that fetch it, whatever the API's pagination style: ```json { "items": [ ... ], "hasMore": true, "nextPage": { "cursor": "cur_2", "limit": 20 } } ``` Results are capped at 64,000 characters (about 16k tokens, under every major client's own limit) so the agent sees an explanation instead of a mid-JSON cut. Over the cap, a page is cut to whole items and gains a `truncated` note saying how many were omitted and how to continue: offset and last-id pagination styles resume exactly at the cut through `nextPage`; cursor and page styles say to call again with a smaller page size. A plain object loses its largest keys, each replaced by a marker that names the `fields` call that fetches it alone. The package's server takes the cap from `_MCP_MAX_RESULT_CHARS`. API failures come back as tool results with `isError: true`, carrying the typed error, a stable code, your API's own error payload, and what to do next, so the agent can branch without parsing prose: ```json { "error": "NotFoundError", "code": "NOT_FOUND", "message": "...", "status": 404, "body": { ... }, "docs_url": "https://docs.acme.example", "next_steps": ["Check the id; list the resource first to find the right one."] } ``` The codes are the generated CLI's: `NO_AUTH`, `AUTH_INVALID`, `PLAN_LIMIT`, `NOT_FOUND`, `INVALID_REQUEST`, `RATE_LIMITED`, `SERVER_ERROR`, `NETWORK_ERROR`, `VALIDATION_FAILED`, plus `INVALID_ARGUMENTS` for the checks above. A 401 names where credentials come from on the transport in use (the environment variable and `login` on stdio, the `Authorization` header over HTTP); a 429 carries the wait. See [Output and exit codes](https://typeship.dev/docs/platforms/cli#output-and-exit-codes) for what each code means. JSON-RPC protocol errors are reserved for unknown tools, unknown methods, and malformed requests. ### Files: binary responses and uploads A binary response (an image, a PDF, an archive: anything the API returns that is not JSON or text) never comes back as `{}`. Images up to 4 MB come back as an `image` content block the model can look at, with a text summary (`media_type`, `bytes`) beside it. Other binaries depend on where the server runs: * The package's server, run as a process on the agent's machine (stdio, or `--http` launched there), saves the bytes to a per-server directory under the OS temp dir and returns `{ saved_to, media_type, bytes }`, the same way the CLI's `--out` materializes files. * A remote server (the [hosted endpoint](#hosted-endpoint), or the Cloudflare worker) has no disk of the agent's. Binaries up to 1 MB are embedded as a base64 `resource` block; larger ones return a description with the size and the advice to fetch through the SDK, the CLI, or the package's local server. Uploads go the other way. Operations with a `multipart/form-data` body or a raw binary body are tools on the package's local server, where each file argument is a local path the server reads and sends (`"image": "/Users/me/avatar.png"`); an unreadable path is an argument error before anything is sent. On a remote server they are not tools, since the server cannot see the agent's files; the argument description and the server instructions say so. Generation reports which operations are affected. ## Read-only and narrower servers Agents that should look but not touch get a server that cannot write. `--read-only` (or `_MCP_READ_ONLY=1`) keeps only read operations: GET and HEAD, GraphQL queries, and POST operations the spec names as reads. Writes are out of the callable set, not merely unlisted: a write called by name is an unknown tool, and `execute` does not know it either. The server's instructions say it is read-only and how many operations that hides. ```bash acme mcp install --claude --read-only # registers node dist/mcp.js --read-only ``` `--tools accounts,transfers.create` (or `_MCP_TOOLS`) narrows the server to named resources, tool names, or dotted `resource.method`, for agents that need a corner of a large API without the meta tool shape. Both switches compose with `mcp.tool_mode`. The [hosted endpoint](#hosted-endpoint) serves its read-only twin at `/mcp//readonly`. ## Instructions for agents `server/discover` returns instructions that agents read once when they connect: what the tools are, how arguments and results behave, where credentials come from on this transport, whether the server is read-only, and, when the API has an operation that returns the caller (the CLI's `whoami` target, or `cli.whoami_operation` in config), which tool to call first to learn whose credential this is. Add your own with `mcp.instructions` in the project's [config](https://typeship.dev/docs/projects/config) (up to 2,000 characters): what to call first, conventions the spec does not state, what not to do. The package's server and the hosted endpoint both carry it. `serverInfo` names your docs site as `websiteUrl` when one is configured. Generation reports the size of `tools/list` as agents pay for it (characters and an approximate token count, in the generation's `meta`), and warns above 80 KB, since every client loads the list once per session. ## OAuth discovery When your spec declares an OAuth2 flow, the HTTP transport serves RFC 9728 protected-resource metadata at `/.well-known/oauth-protected-resource`, naming the authorization server derived from the flow's authorization and token URLs. A request that arrives without an `Authorization` header, on a server holding no environment credentials, receives a `401` whose challenge points at that metadata: ```http HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://mcp.acme.example/.well-known/oauth-protected-resource" ``` That challenge is how MCP clients discover where to obtain a token. The OAuth flow itself runs against your authorization server. The MCP server never mints, stores, or validates tokens. It forwards the caller's bearer token to your API, which validates it like any other request. ## Docs for agents Two layers back `search_docs` and `read_docs`, and the CLI's `docs` command: 1. **Reference.** Every operation's summary, description, and argument table, generated from your spec and shipped in the package. Works offline. 2. **Guides.** Your documentation site, read through `llms.txt` and `llms-full.txt`. Most docs hosts publish these automatically. The URL resolves in this order: `acme config set docs-url`, then `docs_url` in the project's [config](https://typeship.dev/docs/projects/config), then your spec's `externalDocs.url`. Nothing is fetched unless a docs tool or command runs. Fetches time out after 10 seconds. ## Host it yourself Every package generated with the `mcp` platform includes a Cloudflare Worker entry point (`src/worker.ts`) and a preconfigured `wrangler.toml`. Running a remote MCP server on your own account is one command: ```bash npx wrangler deploy ``` Callers' `Authorization` headers pass through to your API, so the Worker holds no secrets by default. Use `npx wrangler secret put ACME_TOKEN` when the server itself should authenticate. A self-hosted deployment serves the spec it was generated from. The [hosted endpoint](#hosted-endpoint) is the always-current alternative that never needs redeploying. ## Hosted endpoint _Available on Enterprise. typeship runs this service for you, priced by contract._ The server in your package is something your users install and run. The hosted endpoint is the same server, run by typeship, at a URL your users paste into their agent client: ```text https://typeship.dev/mcp/ ``` Turn on **hosted MCP endpoint** under the MCP server platform in [project settings](https://typeship.dev/docs/projects#platforms). It needs the MCP server platform on and Enterprise. The URL appears next to the switch, and on the API as `mcp_url`. Turning the MCP server platform off turns the endpoint off with it. * Tools are built from the project's current spec with [spec patches](https://typeship.dev/docs/projects/spec-patches) applied. Changes are picked up within five minutes. Nothing to regenerate, nothing to redeploy. * Tool calls run through the same request runtime every generated SDK ships: the same query and body encoding, retries with `Retry-After`, per-operation retry policies, idempotency keys, and page walking. A tool behaves the same on the hosted URL and in the package. * Each caller's `Authorization` header passes through to your API. typeship holds no credentials for your API. * Tool calls are rate limited per caller (by bearer credential, else by IP) at 120 per minute and per endpoint at 1,200 per minute; over the limit is `429` with `Retry-After`. `tools/list` and `server/discover` are not limited. * Every tool call is counted per day and per tool. Project settings shows the last 30 days next to the endpoint URL: calls, errors, rate-limited calls, and average upstream latency. * The same `search_docs` and `read_docs` tools ship, backed by your spec and your docs site. * The project's `mcp.tool_mode` and `mcp.instructions` [config](https://typeship.dev/docs/projects/config) apply, so large APIs can serve the three-tool `meta` shape and agents read your guidance on connect. * `/mcp//readonly` is the same endpoint with every write left out of the callable set, for agents that should only read; it serves its own protected-resource metadata at `/mcp//readonly/oauth-protected-resource`. * Arguments are checked and coerced, results are projected with `fields` and capped in size, and errors carry codes and next steps, exactly as in the package. * When your spec declares OAuth2, unauthenticated requests get the standard discovery challenge, with protected-resource metadata at `/mcp//oauth-protected-resource`. Any client that speaks Streamable HTTP can add the URL. Your CLI can write the entry for the common ones: ```bash acme mcp --url https://typeship.dev/mcp/ --claude acme mcp --url https://typeship.dev/mcp/ --cursor ``` See [Connect MCP clients](https://typeship.dev/docs/guides/mcp-clients) for the manual configuration. ### Hosted or self-hosted | | Hosted endpoint | Your package's server | | ----------- | ------------------------------------------ | ------------------------------------------------ | | Runs on | typeship | Your users' machines, or your Cloudflare account | | Spec | Always the project's current spec | Frozen at generation | | Credentials | Passed through per request | Environment, or saved by `acme login` | | Rate limit | 120/min per caller, 1,200/min per endpoint | None built in | | Usage | Counted, shown in project settings | Not collected | | Setup | Paste a URL | Build the package, or `npx wrangler deploy` | Both share tool names, input schemas, argument checking, result shaping, the error contract, and the docs tools. Point external users at the hosted URL, and keep the package's server for local development and for users who need the server inside their own network. Upstream requests from the hosted endpoint time out after 30 seconds. ## Protocol notes * Protocol revision `2026-07-28` only. Every request carries `params._meta["io.modelcontextprotocol/protocolVersion"]` and `["io.modelcontextprotocol/clientCapabilities"]`; a request without them is `-32602`, a request for another version is `-32022` with the supported list in `error.data`. Every result carries `resultType: "complete"` and the server's identity in `_meta`. `server/discover` answers with the supported versions, the `tools` capability, and short instructions. * `tools/list` results carry `ttlMs` (one hour: the tool set is fixed at generation) and `cacheScope: "public"`, so clients and shared caches may hold them. Tools come back in spec order, so the list is stable across calls. * Cancellation: on stdio, `notifications/cancelled` for an in-flight request suppresses its response; on HTTP, closing the connection is the signal. Other notifications are accepted and ignored. * No legacy handshake. A client that opens with `initialize` (protocol revisions `2025-11-25` and earlier) gets an error that names `2026-07-28`, as the spec asks of modern-only servers. Claude Code and Claude Desktop speak `2026-07-28` today; Cursor (3.2) does not yet. Clients built on the official SDKs speak it when they enable version negotiation (TypeScript: `versionNegotiation: { mode: "auto" }` or a pin); with the SDK's default settings they still open with `initialize` and will not connect. Ask your API's consumers to update their client if that happens; the error message tells them why. * The server exits when its stdin closes. * On stdio, one JSON-RPC message per line on stdin and stdout. * Operations that stream server-sent events are not exposed as tools (a tool result is one value, not a stream); they are still SDK methods and CLI commands, and generation reports which ones. Operations with binary or multipart request bodies are tools on the package's local server only; see [Files](#files-binary-responses-and-uploads). --- # Overview Source: https://typeship.dev/docs/projects.md A project is one spec and everything typeship generates from it: languages, platforms, package names, destinations, and the history of every generation. A project is one spec and its lineage. You point it at a spec once, choose the languages and platforms it should produce, and tell it where the packages go. From then on, every generation is recorded, every spec version is kept, and every change to the spec turns into a pull request. ## Create a project In the [console](https://typeship.dev/console), choose **new project**, then: 1. Give it a name. Repository and package defaults are derived from it, so name it after the API. 2. Paste the spec URL. typeship fetches it server-side on every generation, so the URL must be publicly reachable. Swagger 2.0, OpenAPI 3.0 and 3.1, and GraphQL are accepted. See [Spec compatibility](https://typeship.dev/docs/reference/spec-compatibility). 3. Pick platforms: an SDK in TypeScript, Python, or Go, plus the CLI and MCP server. TypeScript alone is the default. Each platform is billed per month on Pro. > **For AI agents:** Everything here is one call: > > `typeship projects create --name "Acme API" --spec-url --languages '["typescript","python"]' --platforms '["sdk","cli"]' --destinations '{...}'` > > . > > `typeship projects create --help` > > lists every field; > > `typeship projects update ` > > changes any of them later. The project opens on its settings page, where you can switch the source to a GitHub repository, turn platforms on and off, and set destinations. You can also create a project from the [typeship API](https://typeship.dev/docs/typeship-api/api) with every field in one call. ## From an anonymous generation An anonymous `POST /v1/generate` or `typeship generate run` from a spec URL leaves a claim link behind (`claim.url` in the response, also noted by the typeship CLI in `.typeship/claims.json`). Open it signed in and choose **claim into my organization**: the run becomes a project here with the same spec URL, language, platforms, package name, and config, ready to regenerate. Platforms beyond your plan are left off and can be turned on in settings. Claims last seven days and can be taken once. Nothing else about the anonymous run is kept. > **For AI agents:** After an anonymous > > `typeship generate run --spec '{"url":...}'` > > , read > > `claim.url` > > from the response (or > > `typeship doctor` > > ) and hand it to the user; that is how an unauthenticated run becomes a linked project without a key ever crossing the conversation. ## Organizations Projects, API keys, and the plan belong to an organization, not to you. Your first organization is created when you sign up, and the organization menu at the top of the console sidebar switches between the ones you belong to. **Manage organization** opens members and invitations: invite a teammate by email and they see the same projects and generation history. Admins alone change billing, delete projects, and revoke keys other members created; members do everything else. ## Spec sources A project has exactly one source. **A URL.** typeship fetches it, follows redirects, and times out after 15 seconds. Specs up to 10MB. No credentials are sent, so the URL must serve the spec without auth. If the URL answers with something that is not a spec, typeship tries it as a GraphQL endpoint and runs the introspection query. **A file in a GitHub repository.** Install the typeship GitHub App on the repository, then set the source to **github repo** with the repository (`acme/acme-api`) and the spec path (`openapi/api.yaml`). Pushes to the default branch that touch that path trigger regeneration, and pull requests that touch it get [preview builds](https://typeship.dev/docs/projects/preview-builds). The App needs read and write access to contents, pull requests, and commit statuses. Which source you use decides how changes are detected, and when breaking changes are reported. Both sources get the changelog, the `typeship/semver` status, and the `breaking` label on every regeneration pull request. A repository source also gets preview builds, which report the same diff on the spec's own pull request before it merges. See [Regeneration](https://typeship.dev/docs/projects/regeneration). ## Platforms A platform is one thing typeship generates from the project's spec: an SDK in one language, the CLI, or the MCP server. Project settings lists them under **platforms** with a switch each, grouped as **SDKs**, **CLI**, and **MCP server**. Each platform's own settings sit under its switch, so a setting only shows when the thing it configures is on. **SDKs.** One switch per language: TypeScript, Python, Go. Each language that is on is its own generation, its own package, and its own pull request, with its package name and destination under the switch. A project generates at least one SDK. **CLI, MCP server.** Built on the TypeScript SDK and shipped inside its package, so turning one on turns the TypeScript SDK on with it and keeps it on. Python and Go packages are SDK-only. Under the CLI: login and whoami settings, the support URL, the update notice, and the [webhook relay](https://typeship.dev/docs/platforms/cli#webhooks-listen). Under the MCP server: the tool shape and the [hosted endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint). The hosted features need their platform on and a paid plan. Turning a platform off turns its hosted feature off, and the URL stops answering. Turning a platform off stops generating it and stops its pull requests. Nothing already delivered is removed: files stay in your repositories and published packages stay published. The next pull request for the TypeScript package is generated without it. Every platform that is on is billed per month on Pro. The section shows the count, and the price once billing is on. Free projects run one platform, an SDK in one language, so the other switches show a **Pro** badge until the account upgrades. On the API the same list is `platforms` plus `languages`, and the API applies the same rules: `cli` and `mcp` require `typescript` among the languages, a hosted toggle requires its platform, and more platforms than the plan allows is a 402. ## Package names Every ecosystem gets a name derived from your API's title. The Acme API produces `acme` on npm, `acme` on PyPI (imported as `acme`), and a Go module whose path comes from its destination repository. Brand casing survives: GitHub stays `github`, not `git-hub`. Words that say what a thing is rather than which thing it is (`api`, `rest`, `platform`, `sdk`) are dropped, so "Acme Payments Platform API v2" becomes `acme-payments`. Override any of them per project under **package names**: an npm name or scope (`@acme/api`), a PyPI distribution name, or a Go module path (`github.com/acme/acme-go`). Names must be valid for their registry. The CLI bin and environment variable prefix follow the npm name, so `@acme/api` gives you the `api` bin and `API_TOKEN`. ## Destinations Each language has a destination: a repository and, optionally, a directory in it. Pull requests land there. Two shapes work: * **A dedicated repository per language.** `acme/acme-node`, `acme/acme-python`, `acme/acme-go`. This is what the ecosystems expect. `go get` resolves a module to a repository root, so Go in particular wants its own repository. * **A directory in the spec's repository.** For a TypeScript package, leaving the destination empty falls back to the repository the spec lives in. Python and Go need an explicit repository. Without one, the generation still runs and is recorded, but no pull request opens. ## Generations and history Every run is a generation: one per language, with its trigger (manual, webhook, poll, or preview), its warnings, and every file. The project page lists the fifty most recent. Open one to browse the files, read the warnings, and download the package as a zip. Generations that opened a pull request link to it. Every distinct spec typeship generated from is kept as a spec version, content-addressed by hash, with its source. A generation records which spec version it came from, so a build is reproducible from its real input. Read them back through the [typeship API](https://typeship.dev/docs/typeship-api/api). ## Delete a project Deleting a project removes its generations, spec versions, and relay sessions. Packages already merged into your repositories are unaffected. --- # Regeneration Source: https://typeship.dev/docs/projects/regeneration.md typeship watches your spec, regenerates every package when it changes, and opens a pull request per language with the API changes spelled out. _Available on Pro and Enterprise. The free plan includes one hosted generation._ A generated package is a snapshot of one spec. Regeneration keeps it current. Link a project to its spec once, and every change becomes a pull request in your normal review flow. Nothing lands unseen. ## The loop 1. The spec changes. 2. typeship notices, fetches the spec, applies the project's [spec patches](https://typeship.dev/docs/projects/spec-patches), and generates every language. 3. One pull request opens per language in that language's destination repository, with the API changes listed in the body and `CHANGELOG.md` updated in the destination. 4. You review and merge. ## How changes are detected **Repository sources.** A push to the default branch that touches the spec path triggers regeneration immediately. **URL sources.** typeship polls the URL every 30 minutes and regenerates when the content changes. The poll also acts as a backstop for repository sources in case a webhook is missed. > **For AI agents:** Force a run with > > `typeship projects generate ` > > (URL-sourced projects); toggle the switch with > > `typeship projects update --auto-regen false` > > ; read history with > > `typeship projects list-generations ` > > . Both are governed by one switch, **auto-regen**, off by default so the first generation is always one you asked for. Turn it on in project settings (or `--auto-regen true`) once the output is what you want; off, only **generate now** and the API regenerate. Change detection compares the spec's hash against the last generation. An unchanged spec is skipped. **Generate now** in the console always runs, so you can force a rebuild after changing settings. ## The pull request Each pull request is opened from a branch named `typeship/regen--` against the destination's default branch. The title is `Regenerate acme (TypeScript, 42 operations)`. The body carries: * The language, package name, operation count, line count, and destination directory. * The first generation warnings, if any. * **API changes**: what was added, removed, or changed since the last generation, with breaking changes marked. * A note that files removed from the generated package are not deleted by the revision. Delete stale files in your review if a resource disappeared. The first generation of a project has no changes to report, so its pull request has no changelog section. ## The changelog Every regeneration pull request reports what it does to that repository's API surface, measured against the surface that is actually merged there. The result is written to the pull request body and prepended to `CHANGELOG.md` in the destination directory: ```md ## 2.4.0 (2026-08-18) (1 breaking) ### Added - `accounts.close()`: POST /accounts/{id}/close ### Removed (breaking) - `accounts.archive()`: POST /accounts/{id}/archive ### Changed - `accounts.list()`: GET /accounts - **breaking** param: status is now required ``` The changelog comes from the real surface diff, not from commit messages, so it is accurate even when the spec was edited by hand. **The baseline is the destination.** Each pull request commits `.typeship/surface.json` next to the package: the API surface and version that shipped. The next regeneration reads that file from the destination's default branch and diffs against it. A regeneration pull request that was closed without merging therefore never becomes the next one's point of comparison; what the body says will change is what merging it changes. A destination that has no manifest yet is measured against the previous generation, and the pull request says so and adds the file. ## Breaking changes A regeneration pull request carries two signals beyond the changelog: * **A commit status** named `typeship/semver`. It fails when the diff has breaking changes and the package version does not bump the major (the minor, before 1.0). It passes otherwise, with the counts in its description. The first tracked regeneration of a destination has no previous version to compare with, so it passes with a note. * **A `breaking` label** whenever the diff removes a method or field, changes a type, or makes an input required. The label is added when the GitHub App can label pull requests in that repository, and skipped quietly when it cannot. The status is advisory by default. Make it a required check in the destination's branch protection and a regeneration that breaks the SDK without a version bump cannot merge until `info.version` in the spec moves. Leaving it advisory is the right default for most teams: by the time the pull request opens the API has already changed, and an SDK that cannot catch up to it helps nobody. Which repository gets which signal: | | Spec repository | Destination repositories | | -------- | --------------------------------------------------- | ------------------------------------------------------ | | Question | Should this change happen? | What does this do to the SDK's version? | | When | On the pull request, before the spec merges | On the regeneration pull request, after | | Signal | [`typeship/preview`](https://typeship.dev/docs/projects/preview-builds) | `typeship/semver`, the `breaking` label, the changelog | | Needs | A [repository source](https://typeship.dev/docs/projects/#spec-sources) | A destination | A project with a URL source gets the destination signals and nothing before merge. When breaking changes land in such a project, the pull request body and the console say so and link to the settings page where the spec's repository can be linked. > **For AI agents:** Read a generation's diff with > > `typeship projects list-generations ` > > ; > > `breaking_count` > > , > > `semver` > > , and > > `changelog` > > are in each generation's meta. ## Destinations Each language's pull request goes to that language's destination. See [Destinations](https://typeship.dev/docs/projects/#destinations). If a pull request cannot be opened (no repository configured for that language, or the GitHub App is not installed on it), the generation still succeeds and is recorded in the console. The package is still downloadable there. ## Manual regeneration Any project can be regenerated on demand: * **Generate now** on the project page runs the full loop, including pull requests. * `POST /v1/projects/{id}/generations` through the [typeship API](https://typeship.dev/docs/typeship-api/api) regenerates a URL-sourced project and returns the files, without opening pull requests. Use it when you want to drive the packages into your own pipeline. ## Where custom code belongs The generated package is wholly owned by the generator. Regeneration replaces it, and anything you wrote inside it is destroyed. Wrap the client in a module you own instead. See [Extend the client](https://typeship.dev/docs/guides/customize). ## Versioning The generated package version follows the spec's `info.version`. When that value is not semver (for example `v2` or a date), the version is `0.1.0`. Bump `info.version` in the spec to release, and the next pull request carries the new version. If you publish to a registry, that version is what ships. See [Publish your packages](https://typeship.dev/docs/guides/publish). The `typeship/semver` status holds the version to the diff: a breaking change needs a new major, and the pull request names the version to set when it does not have one. --- # Preview builds Source: https://typeship.dev/docs/projects/preview-builds.md Every pull request that touches your spec gets a generated package, a surface diff, and a commit status that fails on breaking changes. _Available on Pro and Enterprise. Preview builds are unlimited._ Preview builds answer "what will this spec change do to the SDK?" before anyone merges. When a pull request in the spec's repository touches the spec path, typeship generates from the PR's head, diffs the API surface against the base branch, and reports on the PR itself. Preview builds need a [repository source](https://typeship.dev/docs/projects/#spec-sources). They run on `opened`, `synchronize`, `reopened`, and `ready_for_review`. ## What lands on the pull request **One sticky comment**, updated on every push: * The verdict up top: `No breaking changes if this PR merges.` or `2 breaking changes if this PR merges.` * **Added**: new methods. * **Removed (breaking)**: methods that disappear. * **Changed**: per method, what changed, with breaking changes marked: a parameter that became required, a type that changed, a field that was removed, a return type or pagination shape that changed. * **New warnings** and **Resolved warnings**, so a spec fix that clears a warning shows up as progress. * A link to the full generated package in the console. **A commit status** named `typeship/preview`. It is `success` when the change is additive and `failure` when it removes methods, changes types, makes inputs required, or when the head spec fails to generate. Make it a required check to block breaking merges, or leave it advisory. ## What counts as breaking * A method removed. * A parameter or body field added as required. * A parameter, body field, or return type whose type changed. * A parameter or field that became required. * A parameter or field removed. * Pagination detected differently. Additions with optional inputs, and changes to an operation's HTTP method or path that keep the same method name, are reported as changes but not marked breaking. ## Before merge and after Preview builds are the gate: they run on the spec repository, before the change exists anywhere else, which is the only moment a breaking change is still cheap to avoid. The regeneration pull request that follows a merge carries the other half, a `typeship/semver` status and a `breaking` label in each destination, so the SDK's version honors what changed. See [Breaking changes](https://typeship.dev/docs/projects/regeneration#breaking-changes). A project with a URL source gets the second half only. ## Details * Spec patches apply to both sides of the diff, so the comment reflects the spec change, not the patches. * The preview package is generated for TypeScript with the project's platforms. * Preview generations appear in the project's history with the `preview` trigger and do not count against the hosted generation allowance. * If the PR does not change the spec's content, no comment is posted. --- # Spec patches Source: https://typeship.dev/docs/projects/spec-patches.md Fix a spec you cannot edit upstream. Patches apply before every generation, and a patch that stops matching is reported, never silently dropped. Some specs have a wrong type, a name that reads badly in code, or a field that should not be there, and the fix upstream is weeks away. Spec patches let a project carry small fixes that apply to the spec before every generation. The source stays untouched. The generated packages get the corrected document. ## Shape > **For AI agents:** `typeship projects update --spec-patches '[{"op":"set","path":"/paths/~1accounts/get/operationId","value":"listAccounts","reason":"stable name"}]'` > > replaces the whole list; read the current one first with > > `typeship projects get ` > > . Then > > `typeship projects generate ` > > and diff the warnings. A patch is a JSON object with an operation and a JSON Pointer path. Add them under **spec patches** in project settings, or through `spec_patches` on the [typeship API](https://typeship.dev/docs/typeship-api/api). Up to 50 per project. ```json [ { "op": "set", "path": "/components/schemas/Account/properties/id/type", "value": "string", "reason": "ids are strings" }, { "op": "rename", "path": "/components/schemas/Acct", "to": "Account" }, { "op": "remove", "path": "/paths/~1internal~1debug" }, { "op": "append", "path": "/servers", "value": { "url": "https://sandbox.acme.example.com/v1", "description": "Sandbox" } } ] ``` | Op | Does | Requires | | -------- | ----------------------------------------------------------------------------------------------------------- | -------- | | `set` | Replaces the value at the path. Creates a missing final key when the parent exists and the path is literal. | `value` | | `append` | Pushes onto the array at the path. | `value` | | `remove` | Deletes the key or array element at the path. | | | `rename` | Renames an object key. Refuses if the destination exists. | `to` | `reason` is optional and shows up in the console and in warnings, so future you knows why the patch exists. ## Path patterns Paths are JSON Pointers with three additions for bulk fixes: * `*` matches any child. * `**` matches any depth, including zero. * `[name=account_id]` matches children that are objects whose `name` property is `account_id`. ```json { "op": "set", "path": "/paths/**/parameters/[name=account_id]/schema/type", "value": "string" } ``` That one patch retypes every `account_id` parameter in the document. Renaming a schema under `/components/schemas` (or `/definitions` in Swagger 2.0) rewrites every `$ref` to it, so nothing dangles. ## Where patches apply Everywhere a project's spec is read: * Every generation, whatever the trigger. * Both sides of [preview builds](https://typeship.dev/docs/projects/preview-builds), so PR diffs reflect the spec change rather than the patches. * The [hosted MCP endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint). Project settings link to **the patched spec**: the live source with your patches applied, exactly as generation sees it, with a count of applications and any diagnostics. ## When a patch misses A patch whose target no longer exists is skipped and reported as a warning on the generation: ```text Spec patch matched nothing: not applied: set /components/schemas/Acct/properties/id/type (ids are strings) ``` Nothing drifts silently. If the upstream spec fixes the problem, the warning tells you the patch can go. ## Limits * Patches address OpenAPI documents. GraphQL schemas generate unpatched, with a warning when patches are configured. * Patches are not a transform language. There is no templating and no conditional logic, on purpose. When a fix needs more than set, append, remove, and rename, fix the spec. --- # Config Source: https://typeship.dev/docs/projects/config.md Everything typeship needs beyond the spec, in one object: global parameters, retry tuning, pagination rules, and how the generated CLI and MCP server behave. Some things a spec cannot say. Which parameters every call carries. Which statuses your API wants retried. How a list endpoint pages when the shape is unusual. Which endpoint `whoami` should call, and where your docs live. A project's config holds all of it, outside the spec. Nothing is written into your OpenAPI document, so there are no vendor extensions to maintain and nothing to strip before publishing the spec. Config is one object. Set it in project settings, or with the `config` field on the [typeship API](https://typeship.dev/docs/typeship-api/api). > **For AI agents:** `typeship projects update --config '{"retries":{...},"pagination":{...},"cli":{...},"mcp":{...},"docs_url":"https://docs.acme.example"}'` > > replaces the whole object, so send every key you want kept. The same > > `--config` > > works on > > `typeship generate run` > > for a one-off. The console and the API use the same snake\_case shape, so a block is copyable between them, and the same object works as `config` on an ad hoc `POST /v1/generate`. ```json { "globals": ["account_id", "api-version"], "retries": { "max_retries": 3, "statuses": [429, 503], "initial_delay_ms": 500, "max_delay_ms": 8000, "operations": { "createCharge": { "retry_non_idempotent": true }, "GET /health": { "disabled": true } } }, "pagination": { "listEvents": { "style": "cursor", "items_field": "records", "cursor_param": "after", "next_cursor_field": "next" }, "GET /audit": false }, "cli": { "whoami_operation": "users.me", "oauth_client_id": "acme-cli", "oauth_scopes": ["read", "offline_access"], "support_url": "https://github.com/acme/acme-node/issues/new" }, "mcp": { "tool_mode": "auto" }, "graphql": { "endpoint": "https://api.acme.example/graphql", "auth": "basic" }, "docs_url": "https://docs.acme.example" } ``` `globals`, `retries`, `pagination`, `graphql`, and `docs_url` apply to every language and every platform in the project. `cli` and `mcp` configure the CLI and the MCP server, which are TypeScript artifacts, and the console shows them under those platforms. Names under `globals`, `retries`, and `pagination` that match nothing in the spec produce a warning on the generation, so a typo cannot silently do nothing. The `cli`, `mcp`, and `docs_url` half is validated when you save: a bad URL or an unknown tool mode is rejected outright. ## Global parameters `globals` lists the wire names of query or header parameters that every call should carry. Each becomes a client option, typed from the parameter's schema, applied to every operation that accepts it, with per-call values winning. Up to 20. ```ts const client = new AcmeClient({ accountId: "acct_123", apiVersion: "2026-08" }); ``` The generated CLI reads the same values from `ACME_ACCOUNT_ID`-style environment variables and accepts `--account-id` per invocation. The MCP server reads the environment variables. Path parameters cannot be globals. Signatures stay positional. ## Retry tuning `retries` sets the root policy and, under `operations`, overrides keyed by `operationId` or `"METHOD /path"`. | Field | Meaning | | ---------------------------------- | ------------------------------------------------------------------------------ | | `max_retries` | Retries after the first attempt. 0 to 10. | | `statuses` | Replaces the default retryable set (`408`, `429`, `500`, `502`, `503`, `504`). | | `initial_delay_ms`, `max_delay_ms` | The backoff window. | | `retry_non_idempotent` | Retry POST and PATCH too. | | `disabled` | No retries for this scope. | Resolution at runtime is per-call option, then the operation's policy, then the root policy, then the SDK defaults. See [Retries and timeouts](https://typeship.dev/docs/platforms/sdk#retries-and-timeouts). ## Pagination rules Pagination detection is heuristic. When it guesses wrong for an endpoint, or your API pages in a way it does not recognize, pin the rule under `pagination`, keyed by `operationId` or `"METHOD /path"`. | Field | Meaning | | ----------------------------------------------------------------- | -------------------------------------------------------------------- | | `style` | `cursor`, `cursorFromLastId`, `page`, or `offset`. Default `cursor`. | | `items_field` | The array property holding items. Required. | | `cursor_param`, `next_cursor_field`, `has_more_field`, `id_field` | Cursor styles. | | `page_param`, `offset_param`, `limit_param` | Page and offset styles. | Set a key to `false` to turn pagination off for that operation. Malformed or unmatched rules fall back to detection with a warning. Up to 100 keys. ## Generated CLI `cli` shapes the commands your CLI ships with. Under **generated tooling** in project settings. | Field | Effect | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `whoami_operation` | Pins the `resource.method` that `whoami` calls when auto-detection picks the wrong endpoint. | | `oauth_client_id`, `oauth_scopes`, `oauth_audience` | Enable the device-flow `login` and shape its token request. Include `offline_access` in the scopes if your authorization server gates refresh tokens behind it. | | `update_notice` | Opts the CLI into a once-a-day registry check that suggests `upgrade`. Off by default, so the CLI never phones home unless you say so. | | `support_url` | The target of the `feedback` command. GitHub issue URLs get a prefilled title and environment details. | ## MCP server `mcp.tool_mode` is `auto`, `operations`, or `meta`. `meta` collapses per-operation tools into `search_docs`, `read_docs`, and `execute` so large APIs do not flood an agent's context window. `auto` switches to `meta` above 100 operations. It applies to the server in your package and to the [hosted endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint) alike. See [Tool mode for large APIs](https://typeship.dev/docs/platforms/mcp#tool-mode-for-large-apis). `mcp.instructions` is text appended to the server's instructions, which agents read once when they connect: what to call first, conventions the spec does not state, what not to do. Up to 2,000 characters. The package's server and the hosted endpoint both carry it. See [Instructions for agents](https://typeship.dev/docs/platforms/mcp#instructions-for-agents). `mcp.tool_descriptions` is an object keyed by operationId or `"METHOD /path"` whose values replace the tool description typeship derives for that operation, for flows the spec cannot describe (a multi-step upload, a slow report). Up to 200 keys of 600 characters. Keys that match no operation are reported as generation warnings. ```json { "mcp": { "tool_descriptions": { "createUpload": "Step 1 of 3: reserve a slot, PUT the bytes to upload_url, then call uploads_finish.", "GET /reports": "Slow (10 to 30 s); pass fields to keep the result small." } } } ``` ## GraphQL schemas A GraphQL schema says nothing about where it is served or how requests authenticate, so `graphql` carries both. It is ignored for OpenAPI specs. ```json { "graphql": { "endpoint": "https://payments.braintree-api.com/graphql", "environments": [ { "name": "sandbox", "url": "https://payments.sandbox.braintree-api.com/graphql" }, { "name": "production", "url": "https://payments.braintree-api.com/graphql" } ], "auth": "basic", "title": "Braintree" } } ``` `endpoint` becomes the generated client's default `baseUrl`. It defaults to the spec URL when that URL is the endpoint itself. `environments` name additional endpoints, each a client environment. `auth` is `bearer` (the default, sent as `Authorization: Bearer`), `basic` for key-pair APIs where the public key is the username and the private key the password, `api_key` with `api_key_header` for a header, or `none`. `title` names the package and client (`braintree`, `BraintreeClient`) and defaults to a name taken from the endpoint's host. See [Generate from GraphQL](https://typeship.dev/docs/guides/graphql). ## Docs site `docs_url` is your API's documentation site. Three surfaces read it through your site's `llms.txt`: the CLI's `docs` command, the MCP server's `search_docs` and `read_docs` tools, and the package's `AGENTS.md`. It defaults to the spec's `externalDocs.url`. Most docs hosts publish `llms.txt` and `llms-full.txt` automatically. ## Replace, do not merge On the API, `config` replaces the whole object. Send every key you want kept, and `null` to clear everything. The console does the same on save. --- # Add a package to your repo Source: https://typeship.dev/docs/guides/add-to-your-repo.md Where a generated package goes in your repository, how to depend on it, and how to build it. For projects with a destination configured, the pull request already did this. A generated package is a complete package for its ecosystem: readable source, zero runtime dependencies, and a build. This guide covers placing one by hand. When a project has a [destination](https://typeship.dev/docs/projects/#destinations) configured, typeship's pull request puts the package in place, one per language, and updates it on every spec change, so most of this page is only for downloaded zips. **TypeScript** ### Where to put it * In a monorepo with workspaces: `packages/acme`. * In a single-package repository: `vendor/acme`. * Any path works. The import name comes from `name` in the generated `package.json`, not from the directory. ### Depend on it As a file dependency: ```bash npm install ./vendor/acme # or pnpm add ./vendor/acme # or yarn add file:./vendor/acme ``` Or as a workspace member: ```json title="package.json" { "workspaces": ["packages/*"] } ``` ```json title="apps/api/package.json" { "dependencies": { "acme": "*" } } ``` Installing also puts the package's bins on your PATH when those platforms were generated: `acme` for the CLI and `acme-mcp` for the MCP server. ### Build it The package ships as TypeScript source and must be compiled before anything imports it. `main`, `types`, and `exports` point into `dist/`: ```bash cd vendor/acme npm install # typescript and @types/node, dev only npm run build # tsc -> dist/ ``` After the build, `dist/` holds the compiled JavaScript, `.d.ts` declarations, and declaration maps, so editors jump from your code into the package's source. ### ESM only * The package declares `"type": "module"` and its `exports` map exposes an `import` entry only. `require()` is not supported. * Node 18 or newer. * Works with modern bundlers. It is tree-shakeable: one module per resource and `"sideEffects": false`, so bundlers drop the resources you never call. **Python** ### Where to put it Anywhere. The package directory is `acme/` next to its `pyproject.toml`. A `vendor/acme` directory in your repository is a fine home. ### Depend on it ```bash pip install ./vendor/acme # or, in a requirements file: ./vendor/acme ``` Or, once you [publish to PyPI](https://typeship.dev/docs/guides/publish), `pip install acme`. ### Nothing to build `pyproject.toml` declares `dependencies = []` and `requires-python >= 3.11`. Import and go: ```python from acme import AcmeClient ``` **Go** ### Where to put it Go modules live in their own repository. `go get` resolves a module path to a repository root, which is why the recommended destination for a Go package is a dedicated repository such as `acme/acme-go`. Set that repository as the Go destination in project settings, and the module path follows. ### Depend on it ```bash go get github.com/acme/acme-go ``` ```go import acme "github.com/acme/acme-go" ``` Until the module is pushed to its repository, `go.mod`'s `replace` directive points at a local directory: ```text title="go.mod" replace github.com/acme/acme-go => ../acme-go ``` ### Nothing to build The module has no `require` block. `go build` compiles it with the standard library alone. ## Keep custom code outside the package Regenerating replaces the entire package. Never put your own code inside it. Wrap the client in a module you own. See [Extend the client](https://typeship.dev/docs/guides/customize). --- # Publish your packages Source: https://typeship.dev/docs/guides/publish.md Publish generated packages to npm, PyPI, and as a Go module under your own name. The packages are yours. typeship maintains them; you own the registry. Publishing the generated packages under your organization is supported and expected. It is your code: readable source, zero runtime dependencies, and nothing that depends on typeship at runtime. typeship's job is to keep the packages current through pull requests. Releasing them is your normal release process. Every generated package carries a header comment and README credit naming typeship as the generator, on every plan. ## Set the names first > **For AI agents:** `typeship projects update --package-names '{"typescript":"@acme/sdk","python":"acme","go":"github.com/acme/acme-go"}'` > > . Set package names per language in project settings under **package names**, or with `package_names` on the API, so pull requests arrive publish-ready: * npm: `@acme/sdk` or `acme` * PyPI: `acme` * Go: `github.com/acme/acme-go` (derived from the Go destination repository) ## Version The package version follows `info.version` in your spec. Bump the version in the spec and the next pull request carries it. When `info.version` is not semver, the version is `0.1.0`, and you set it yourself before publishing. The regeneration pull request checks the version against what changed: its `typeship/semver` status fails when the diff has breaking changes and the major did not bump, and the body names the version to set. See [Breaking changes](https://typeship.dev/docs/projects/regeneration#breaking-changes). **npm** ```bash cd packages/acme npm run build # prepublishOnly runs this too npm publish --access public # --access public for a scoped package ``` The `files` field ships `dist/` and `src/`, so consumers get compiled output and readable source. When the CLI platform is on, `npm install -g @acme/sdk` puts the `acme` bin on the PATH, and the CLI's `upgrade` command starts working against your registry. **PyPI** ```bash cd packages/acme-python python -m build python -m twine upload dist/* ``` `pyproject.toml` is already complete: name, version, `dependencies = []`, and `py.typed`. **Go** Go modules publish by pushing to the repository named in the module path and tagging: ```bash git tag v2.3.0 git push origin v2.3.0 ``` `go get github.com/acme/acme-go@v2.3.0` then resolves. Major versions above 1 need a `/v2` suffix on the module path, which you can set under **package names**. ## Release from the pull request A tidy loop for a dedicated SDK repository: 1. typeship opens a pull request with the regenerated package and the API changes in the body. 2. Review, merge. 3. A release workflow in that repository publishes on merge to the default branch, or on tag. Because `CHANGELOG.md` is maintained in the destination, release notes are already written. ## Registry publishing by typeship typeship does not hold registry tokens or publish on your behalf. Packages reach registries through your pull requests and your release workflow. --- # Extend the client Source: https://typeship.dev/docs/guides/customize.md The generated package is replaced whole on every regeneration. Put your configuration, logging, and domain logic in modules you own that wrap the client. The generated package is wholly owned by the generator. Regeneration replaces the directory, and anything you wrote inside it is gone. That is a feature. It is what makes the next pull request a clean diff of spec changes and nothing else. Customization lives in code you own that wraps the client. The client was designed to make that easy. ## A factory you own Pin the base URL, auth, and defaults in one place so call sites never repeat configuration: ```ts title="src/lib/acme.ts (your code, outside the generated package)" import { AcmeClient } from "acme"; export function createAcmeClient(): AcmeClient { return new AcmeClient({ baseUrl: process.env.ACME_BASE_URL, bearerToken: process.env.ACME_TOKEN!, defaultHeaders: { "Request-Source": "core-team" }, }); } ``` ## Instrument with hooks and `fetch` Logging, metrics, tracing, and proxies belong in the `fetch` option or the hooks, not in generated files: ```ts const client = new AcmeClient({ bearerToken: token, fetch: async (input, init) => { const started = Date.now(); const response = await fetch(input, init); console.log(`${init?.method ?? "GET"} ${String(input)} ${response.status} ${Date.now() - started}ms`); return response; }, onRequest: (context) => { context.headers["Trace-Id"] = crypto.randomUUID(); }, }); ``` Python takes `transport=`, `on_request=`, and friends. Go takes `WithHTTPClient` and `WithOnRequest`. See [Hooks and debug logging](https://typeship.dev/docs/platforms/sdk#hooks-and-debug-logging). ## Domain logic on top Caching, mapping to your own types, and business rules belong in your functions that call the client and pass results on: ```ts import { unwrap } from "acme"; import { createAcmeClient } from "./acme"; const client = createAcmeClient(); export async function activeAccountIds(): Promise { const ids: string[] = []; for await (const account of client.accounts.list({ limit: 100 })) { if (account.status === "active") ids.push(account.id); } return ids; } ``` ## Things that belong in the project, not in code Some customizations change what gets generated. Those live in the project's [config](https://typeship.dev/docs/projects/config) so every regeneration carries them: * Parameters every call should carry: [global parameters](https://typeship.dev/docs/projects/config#global-parameters). * Which statuses to retry and how: [retry tuning](https://typeship.dev/docs/projects/config#retry-tuning). * How an unusual endpoint pages: [pagination rules](https://typeship.dev/docs/projects/config#pagination-rules). * A wrong type or name in a spec you cannot edit: [spec patches](https://typeship.dev/docs/projects/spec-patches). With this split, regeneration never destroys your code. The package directory is disposable, and everything you wrote lives outside it. --- # Generate in CI Source: https://typeship.dev/docs/guides/ci.md Drive typeship from a pipeline with the typeship CLI or API: regenerate a project on your schedule, or generate a package ad hoc and commit it yourself. Projects with a destination regenerate on their own and open pull requests, so most teams never need CI for generation. Reach for it when you want the packages in your own pipeline: to commit them alongside your app, to build them into a container, or to gate a release on a fresh generation. Both paths use typeship's own tooling. This page is about calling **typeship**, not about running your generated CLI. For that, see [In CI](https://typeship.dev/docs/platforms/cli#in-ci) on the CLI page. ## Regenerate a project and pull the files The [typeship CLI](https://typeship.dev/docs/cli) reads its key from `TYPESHIP_TOKEN` and prints JSON, so it drops into any job. Create a key under **api keys** in the console and store it as a secret. ```yaml title=".github/workflows/sdk.yml" name: Regenerate SDK on: workflow_dispatch: schedule: - cron: "0 6 * * 1" jobs: regenerate: runs-on: ubuntu-latest env: TYPESHIP_TOKEN: ${{ secrets.TYPESHIP_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm install -g typeship-ax - name: Regenerate and unpack run: | typeship projects generate prj_your_project_id > generations.json node -e ' const fs = require("fs"), path = require("path"); for (const g of JSON.parse(fs.readFileSync("generations.json")).data) { if (g.status !== "succeeded") { console.error(g.language, g.error); process.exit(1); } for (const f of g.files) { const p = path.join("packages", g.language, f.path); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, f.content); } }' - run: git status --short ``` `projects generate` regenerates a URL-sourced project, records the generation and its spec version, and returns every language's files. It does not open pull requests. Commit the result however your pipeline commits. Large packages return `files_omitted: true` with a `files_index` instead of inline files. Fetch each with `typeship generations get-file --path src/index.ts`. ## Generate ad hoc, no project `generate run` runs the generator on any spec without a project or an account. It is the same call the homepage makes: ```bash typeship generate run \ --spec '{"url":"https://api.acme.example.com/openapi.json"}' \ --platforms '["sdk","cli"]' \ --language typescript > result.json ``` Or with `curl`: ```bash curl -s https://typeship.dev/api/v1/generate \ -H "Content-Type: application/json" \ -d '{"spec":{"url":"https://api.acme.example.com/openapi.json"},"platforms":["sdk"],"language":"python"}' ``` Ad hoc generation is capped at the first 25 operations of a spec and stores nothing. It is for evaluation and small APIs. Anything you want kept current belongs in a project. ## Fail the build on spec drift Your generated CLI's `--validate` flag checks a live API against the spec it was generated from. A smoke job that runs a few read-only commands with `--validate` catches an API that drifted from its published spec before your users do: ```yaml - name: Spec drift check env: ACME_TOKEN: ${{ secrets.ACME_TOKEN }} run: node packages/typescript/dist/cli.js accounts list --validate --limit 5 ``` --- # Connect MCP clients Source: https://typeship.dev/docs/guides/mcp-clients.md Wire your generated MCP server, or the hosted endpoint, into Claude Code, Cursor, and Claude Desktop. Your API's MCP server reaches agents two ways: as a local process the client spawns (the server inside your package, over stdio) or as a remote URL (the [hosted endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint), or a server you deployed with `--http` or on Cloudflare). This guide shows both for each client. Replace `acme` with your package's bin. ## Fastest: let the CLI do it The generated CLI writes the client configuration for you and never puts a token in the file. Run it from the directory of the project that should see the server: ```bash acme login # once; the MCP server shares these credentials acme mcp --claude # ./.mcp.json acme mcp --cursor # ./.cursor/mcp.json acme mcp --claude-desktop # Claude Desktop's config file ``` For the hosted endpoint, add `--url`: ```bash acme mcp --url https://typeship.dev/mcp/ --claude ``` Add `--read-only` to either form for a server that cannot write: the local entry gets the `--read-only` flag, the hosted URL gets `/readonly`. See [Read-only and narrower servers](https://typeship.dev/docs/platforms/mcp#read-only-and-narrower-servers). Claude Desktop takes remote servers as connectors in the app (Settings, Connectors, Add custom connector), not through its config file, so `--claude-desktop --url` prints that instruction instead of writing an entry. Clients must speak MCP `2026-07-28` (per-request `_meta`, `server/discover`); Claude Code and Claude Desktop do. Cursor's MCP client is still on the `initialize` handshake as of Cursor 3.2 and does not connect; see the [protocol notes](https://typeship.dev/docs/platforms/mcp#protocol-notes). Build the package first (`npm install && npm run build`) so `dist/mcp.js` exists. Existing servers in the file are preserved. ## Manual configuration **Claude Code** Local server, in `.mcp.json` at the project root: ```json title=".mcp.json" { "mcpServers": { "acme": { "command": "node", "args": ["/abs/path/acme/dist/mcp.js"] } } } ``` Remote server: ```json title=".mcp.json" { "mcpServers": { "acme": { "type": "http", "url": "https://typeship.dev/mcp/" } } } ``` Run `claude mcp list` to confirm the server is connected. **Cursor** Create `.cursor/mcp.json` in the project root: ```json title=".cursor/mcp.json" { "mcpServers": { "acme": { "command": "node", "args": ["/abs/path/acme/dist/mcp.js"] }, "acme-hosted": { "type": "http", "url": "https://typeship.dev/mcp/" } } } ``` **Claude Desktop** Add a local server to `claude_desktop_config.json` and restart Claude Desktop. macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`. Windows: `%APPDATA%\Claude\claude_desktop_config.json`. ```json title="claude_desktop_config.json" { "mcpServers": { "acme": { "command": "node", "args": ["/abs/path/acme/dist/mcp.js"], "env": { "ACME_TOKEN": "..." } } } } ``` The `env` block is optional when the user has run `acme login`. For a remote URL, add the hosted endpoint as a custom connector in Claude Desktop's settings. ## Credentials A local server resolves credentials like the CLI: environment variables (`ACME_TOKEN`), then whatever `acme login` saved. A remote server forwards each caller's `Authorization` header to your API. When your spec declares OAuth2, clients that support it discover the authorization server through the standard challenge. See [OAuth discovery](https://typeship.dev/docs/platforms/mcp#oauth-discovery). ## Check it works Ask the agent to list the tools, or call one directly: ```text Use the acme server to list my accounts. ``` Every server also answers `search_docs` and `read_docs`, so "how does pagination work in the Acme API?" is answerable in the session when a docs site is configured. --- # Coding agents Source: https://typeship.dev/docs/guides/coding-agents.md Use typeship from Claude Code, Codex, VS Code, and the other agents: the one-line prompt, the runbook, the CLI's agent contract, the hosted MCP server, and the skills. typeship is built to be driven by an agent as readily as by a person: the CLI prints JSON and stops with structured envelopes, the MCP server exposes every operation as a tool, every docs page has a markdown twin, and one hosted file tells an agent how to set everything up. This page is the map. Nothing on it is agent-only; the same commands work at a keyboard. ## The one line Paste this into your agent: ```text Read https://typeship.dev/agents.md and set typeship up for this repo. ``` [/agents.md](https://typeship.dev/agents.md) is a runbook written to the agent: detect the situation, install the CLI, pick a path (no account, has a key, needs a key, REST only, connect the MCP server), verify, report. It works with no account: the first path generates a package from any spec anonymously. ## Set a machine up in one command With a key in hand (create one under **api keys** in the [console](https://typeship.dev/console/keys)): ```bash npx -y typeship-ax@latest init --all -k ak_... ``` `init` stores the key, installs the [typeship skills](#skills), writes the [hosted MCP server](https://typeship.dev/docs/typeship-api/mcp) into every agent client found on the machine (with `${TYPESHIP_TOKEN}` as an env reference, never the key itself), and adds a `typeship` block to `AGENTS.md` (or `CLAUDE.md`) with the auth rules, the discovery commands, and a compact index of every command. Run it again any time; the block is replaced in place. ## What the CLI promises an agent The full contract is on the [CLI platform page](https://typeship.dev/docs/platforms/cli#output-and-exit-codes), and `typeship agent-guide --format json` prints it. In short: * JSON on stdout. Every failure is one envelope on stderr: `{status, issues: [{code, message}], docs_url, next_steps, detail}`; branch on `issues[].code`. * Agent mode (`--mode agent`, `TYPESHIP_MODE=agent`, or no terminal) never prompts and never opens a browser. * Deletes need `--force`; without it the CLI returns `CONFIRMATION_REQUIRED` and the exact command. * `typeship generate run ... --out sdk/` writes a generated package straight into a directory, no account needed. * `typeship auth check`, `typeship doctor`, `typeship help --json`, `typeship docs search `. ## Per harness **Claude Code** ```bash claude mcp add --transport http typeship https://typeship.dev/mcp-oauth # signs you in; see the MCP page for the key form (CI) /plugin marketplace add typeship-ax/skills /plugin install typeship@typeship-skills ``` Or `typeship init --all` for both. In a session: "Read [https://typeship.dev/agents.md](https://typeship.dev/agents.md) and set typeship up for this repo." **Codex** ```bash codex mcp add typeship --url https://typeship.dev/mcp-oauth && codex mcp login typeship codex plugin marketplace add typeship-ax/skills ``` **VS Code** `.vscode/mcp.json`: ```json { "servers": { "typeship": { "type": "http", "url": "https://typeship.dev/mcp-oauth" } } } ``` VS Code signs you in on first use. `typeship mcp install --vscode` writes the key-based entry instead (`/mcp` with `${TYPESHIP_TOKEN}`), for machines without a browser. **Windsurf, Gemini CLI, OpenCode, Zed** `typeship mcp install --windsurf | --gemini | --opencode | --zed` writes each client's config with the hosted URL and the env reference. `typeship mcp` alone prints the entry and which clients were detected. **Claude Desktop, claude.ai** Add `https://typeship.dev/mcp-oauth` as a custom connector (Settings, Connectors); it signs you in. The desktop config file only launches stdio servers, so `typeship mcp --claude-desktop` registers the local `typeship-mcp` instead. Cursor is left out until Cursor speaks MCP 2026-07-28, the only protocol version typeship's servers serve; `typeship mcp install --cursor` writes the entry on request with that note. ## Skills The [typeship skills](https://github.com/typeship-ax/skills) are Agent Skills that wrap the CLI: a router (`typeship`), `typeship-cli`, `typeship-api` (REST without the CLI), `typeship-spec-prep` (make a spec generate well), `typeship-mcp-clients`, and `typeship-ci`. ```bash npx skills add typeship-ax/skills ``` ## Docs for agents * [/llms.txt](https://typeship.dev/llms.txt): every page, one line each. [/llms-full.txt](https://typeship.dev/llms-full.txt): everything in one fetch. * Append `.md` to any docs URL, or send `Accept: text/markdown`, for that page as markdown with frontmatter and absolute links. * `search_docs` and `read_docs` on the [MCP server](https://typeship.dev/docs/typeship-api/mcp) need no key. * Every page has **Copy page**, **View as Markdown**, **Open in Claude**, and **Open in ChatGPT** under its title. ## Without a key An agent with a spec and no typeship account can still get a package: `typeship generate run` and `POST /v1/generate` work anonymously (the first 25 operations, rate limited per address), and the MCP server's `generate_run` does the same. The response's `limits` object says what was held back and where to sign up. Everything else needs a key from the console; the runbook tells the agent to ask for one rather than to guess. --- # Webhooks Source: https://typeship.dev/docs/guides/webhooks.md Declare webhooks in your spec and every package gets typed events, signature verification, a fake-event command, and a local relay. The full loop, end to end. Webhooks are half of most APIs and usually the half SDKs ignore. typeship generates the receiving side from your spec: typed payloads, a verifying parser, a command that sends signed sample events, and a relay that brings real events to a laptop. ## 1. Declare webhooks in the spec OpenAPI 3.1 has a top-level `webhooks` section. On 3.0 specs, typeship reads the established `x-webhooks` convention. Each entry needs a JSON request body schema: ```yaml webhooks: account.updated: post: requestBody: content: application/json: schema: type: object required: [type, account] properties: type: { type: string, enum: [account.updated] } account: { $ref: "#/components/schemas/Account" } ``` A property pinned to a single value (`enum` with one entry, or `const`) becomes the discriminator, so consumers can switch on `event.type`. ## 2. Sign events the standard way The generated verifier follows the Standard Webhooks convention: * Headers `webhook-id`, `webhook-timestamp`, and `webhook-signature`. * Signed content is `id.timestamp.payload`, HMAC-SHA256, base64, sent as `v1,`. Several space-separated signatures are accepted, so keys can rotate. * Secrets are `whsec_` followed by base64. Any other string is used as raw bytes. * Timestamps older or newer than five minutes are rejected. If your API already signs this way, nothing changes. If it does not, adopting the convention is what makes the generated `unwrap` work. ## 3. Consumers verify with the SDK **TypeScript** ```ts const client = new AcmeClient({ webhookKey: process.env.ACME_WEBHOOK_KEY }); export async function handler(req: Request) { const event = await client.webhooks.unwrap(await req.text(), req.headers); switch (event.type) { case "account.updated": return onUpdated(event.account); case "account.closed": return onClosed(event.account_id); } } ``` `unwrap` throws `WebhookVerificationError` on a bad signature, a stale timestamp, or a missing key. `unwrapUnsafe` parses without verifying. Verification uses WebCrypto, so the same code runs on Node, browsers, edge runtimes, and Workers. **Python** ```python client = AcmeClient(webhook_key=os.environ["ACME_WEBHOOK_KEY"]) def handler(request): event = client.webhooks.unwrap(request.body, request.headers) if event["type"] == "account.closed": on_closed(event["account_id"]) ``` **Go** ```go client, _ := acme.New(acme.WithWebhookKey(os.Getenv("ACME_WEBHOOK_KEY"))) func handler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) event, err := client.Webhooks.Unwrap(body, r.Header) if err != nil { http.Error(w, "bad signature", 400); return } if closed, err := event.AsAccountClosed(); err == nil { onClosed(closed.AccountID) } } ``` All three SDKs sign byte-identically. A payload signed by one verifies in the others. ## 4. Test before any real event exists The generated CLI builds a signed sample event from the spec's schemas: ```bash acme webhooks fake # list declared events acme webhooks fake account.updated --forward-to localhost:3000/webhooks ``` The key is `--key`, then `ACME_WEBHOOK_KEY`, then a throwaway. Set the same key in the handler under test and the signature verifies. ## 5. Bring real events to localhost _Available on Pro and Enterprise._ With the [webhook relay](https://typeship.dev/docs/platforms/cli#webhooks-listen) enabled on the project, `acme webhooks listen --forward-to localhost:3000/webhooks` mints a private URL and replays every event sent there with its original headers. Signature verification works unchanged because nothing is re-signed. Register the printed URL as a webhook endpoint in your API and events start arriving. --- # Generate from GraphQL Source: https://typeship.dev/docs/guides/graphql.md A GraphQL schema generates the same package as an OpenAPI spec, on the same runtime: queries on client.query, mutations on client.mutation, typed selections, connections that auto-paginate. typeship accepts a GraphQL schema wherever it accepts an OpenAPI spec. The output is a TypeScript, Python, or Go package with the same client, results, errors, retries, and platforms. The CLI and MCP server are TypeScript, as for every spec. ## Inputs Three shapes work as a project's spec URL or as an ad hoc input: * **SDL**: a `.graphql` schema file. * **Introspection JSON**: the `__schema` payload. * **A GraphQL endpoint URL**: typeship runs the standard introspection query server-side and generates from the result. The endpoint becomes the client's default `baseUrl`. Endpoints with introspection disabled need the SDL instead. ## What the schema cannot say A GraphQL schema has no equivalent of OpenAPI's `servers` and `securitySchemes`. Where the API lives and how requests authenticate come from the project's [config](https://typeship.dev/docs/projects/config#graphql-schemas), or from the endpoint and auth fields the generator shows when it sees a GraphQL input: * **Endpoint**: every request is one `POST` to it, and it becomes the client's default `baseUrl`. A schema fetched from its own endpoint already has one. Named environments (sandbox, production) become client environments. * **Auth**: `bearer` by default, sent as `Authorization: Bearer`. `basic` suits key-pair APIs, with the public key as the username and the private key as the password. `api_key` sends a header. `none` generates no credential option. The generated README, AGENTS.md, CLI, and MCP server all describe the scheme that was chosen. * **Name**: the package and client names (`braintree`, `BraintreeClient`) come from `title`, or from the endpoint's host when it is not set. Headers an API needs on every call, such as a version header, go in `defaultHeaders` on the client. ## The mapping * Each root field becomes an operation. Queries land on `client.query`, mutations on `client.mutation`. Field arguments become a typed body object, required when any argument is non-null. * Object types carry an optional `__typename` literal. Unions, and interfaces with implementers, become a union of their concrete types, so results narrow on `__typename`. * Custom scalars map to `unknown`. Built-ins map to their TypeScript equivalents. * Subscriptions are skipped, with a warning. * Operation documents (files of queries you already wrote) are not an input. Generation works from the schema alone. ## Selections A schema does not say which fields to fetch. Generated methods select every scalar and enum field to depth 2 by default, with a `... on` fragment for every concrete member of a union or interface. Every object-returning method takes an optional `select` argument after its body. A field that returns a scalar takes none. In TypeScript, `select` is a typed object checked against the schema. A typo is a compile error, and the result type narrows to exactly what was picked: ```ts const client = new BraintreeClient({ baseUrl: "https://payments.sandbox.braintree-api.com/graphql", basicAuth: { username: publicKey, password: privateKey } }); const whole = await client.query.transaction({ id: "txn_1" }); // default selection const slim = await client.query.transaction({ id: "txn_1" }, { id: true, amount: true, customer: { email: true } }); // slim.data: { id: string; amount: number; customer: { email: string | null } | null } | null ``` Unions and interfaces take `on`, keyed by concrete type name, and come back discriminated by `__typename`: ```ts const node = await client.query.node({ id }, { id: true, __typename: true, on: { Transaction: { amount: true }, Customer: { email: true } } }); if (node.ok && node.data?.__typename === "Transaction") node.data.amount; ``` Each method's default is exported, so adding a field is a spread: `{ ...queryTransactionSelection, status: true }`. A raw selection-set string (`"{ id amount }"`) is the escape hatch and returns the full type. `Selection` and `Selected` are exported for helpers of your own. In Python the same override is the `select=` keyword with a raw selection set (`client.query.transaction(id="txn_1", select="{ id amount }")`). In Go it is a per-call option (`client.Query.Transaction(ctx, &braintree.QueryTransactionParams{ID: "txn_1"}, braintree.WithSelection("{ id amount }"))`). The CLI exposes it as `--select`, and MCP tools accept a `select` argument. ## Connections Relay-style connections auto-paginate. A field taking `first` and `after` and returning `edges` plus `pageInfo` (with `hasNextPage` and `endCursor`) is detected as a list operation. Iteration yields edges, and pages advance by resending the query with `after` set to the previous page's `endCursor`: ```ts for await (const edge of client.query.transactions({ first: 50 })) { edge.node; // Transaction } ``` ## Errors GraphQL reports failures in-band, as a `200` carrying an `errors` array. The runtime turns that into `GraphQLRequestError`, carrying the full errors array, on the error side of `ApiResult`. Transport failures and undocumented statuses surface as `TransportError` and `UnexpectedApiError` exactly as they do for OpenAPI. Successful payloads are unwrapped: `result.data` is the field's value, not the `{ data: { ... } }` envelope. ## Limits * Runtime validation does not cover GraphQL operations. * [Spec patches](https://typeship.dev/docs/projects/spec-patches) address OpenAPI documents. GraphQL schemas generate unpatched, with a warning when patches are configured. * The free plan's 25-operation allowance counts root fields, in schema order (queries first, then mutations). The generated README says when output was capped. --- # Generate a package from a spec URL Source: https://typeship.dev/docs/workflows/generate-from-a-url.md From a public OpenAPI or GraphQL URL to a typed package in a directory, with or without an account. The commands, then why each one. ## Quick reference ```bash npm install -g typeship-ax # or npx -y typeship-ax@latest ... typeship generate run --spec '{"url":"https://api.acme.example.com/openapi.json"}' \ --language typescript --platforms '["sdk","cli"]' --out sdk/acme # no key needed cd sdk/acme && npm install && npm test # TypeScript: build + one smoke test per operation ``` With a key in `TYPESHIP_TOKEN`, the same command generates the whole spec instead of the first 25 operations. ## Why these commands **`generate run` is stateless.** It runs the generator on the spec and returns the package. Nothing is stored, so it is the right call for a one-off, an evaluation, or a script that commits the output itself. `--out` writes the files where you point it and prints a summary (meta, warnings, `limits`, `claim`) instead of the file contents; without `--out` the whole package comes back as JSON. **Anonymous is fine to start.** Without a key the first 25 operations generate, 20 calls a minute per address. The response's `limits` object says how many operations were left out; `claim.url` is a link that turns this run into a linked project once you sign in, so nothing is lost by starting without an account. **Pick the language first, then platforms.** `--language` is `typescript`, `python`, or `go`. The CLI and MCP server are TypeScript artifacts, so `--platforms` with `cli` or `mcp` only applies there; Python and Go produce the SDK alone. **Read the warnings once.** They name what the spec left out or what was approximated (a relative server URL, a security scheme that did not map, an operation without an id). Most are fixed in the spec or with a [spec patch](https://typeship.dev/docs/projects/spec-patches), not by hand in the output. **Verify before you depend on it.** A TypeScript package builds and ships one smoke test per operation (`npm test` against the spec's shapes, no network); Python compiles (`python -m compileall`); Go builds (`go build ./...`). The package's own `AGENTS.md` and `api.md` are the reference for what you generated. ## What can go wrong | You see | It means | Do | | ------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------- | | `fetch_error` | typeship could not fetch the URL (auth, 404, timeout) | Make the spec public, or paste it: `--spec "{\"inline\":$(jq -Rs . < openapi.yaml)}"` | | `spec_error` / `SPEC_INVALID` | the document is not a usable spec | [Debug a spec](https://typeship.dev/docs/workflows/debug-a-spec) | | `limits.omitted_operations > 0` | the anonymous or free cap | Sign in; a key generates the whole spec | | `RATE_LIMITED` | more than 20 anonymous calls a minute | Wait the seconds named; or send a key | > **For AI agents:** Run exactly the quick reference. If the response carries > > `claim.url` > > , give it to the user. Report what was generated, where, and the warnings in one line. --- # Add typeship to an existing API repository Source: https://typeship.dev/docs/workflows/add-to-an-existing-api-repo.md The repository already has the API and its spec. Put the generated package beside it, wire a linked project, and let pull requests keep it current. ## Quick reference ```bash # 1. once per machine typeship login # approve in the browser; a key is minted for this machine # 2. once per repository: a project linked to the spec file in this repo typeship projects create --name "Acme API" \ --source '{"kind":"repo","repo":"acme/api","path":"openapi.yaml"}' \ --languages '["typescript"]' --platforms '["sdk","cli","mcp"]' \ --destinations '{"typescript":{"repo":"acme/api","directory":"packages/acme-node"}}' # 3. the first generation lands as a pull request; merge it typeship projects list-generations ``` If the spec is served at a URL rather than a file, use `--spec-url https://...` instead of `--source`; typeship polls it every 30 minutes. ## Why these commands **Install the GitHub App on the repository.** A repository source and a repository destination both go through the typeship GitHub App. The console prompts for it when you set a destination; the first `projects create` for a repository you have not connected returns the install link in the error. Install once per GitHub account or organization. **The destination is a repository plus a directory.** Same repository as the API (`packages/acme-node`, `sdk/`) or a dedicated one (`acme/acme-node`). Same-repo keeps the spec and its client in one pull request stream; a dedicated repository keeps release history separate and is what you want when the package publishes to a registry. See [Add a package to your repo](https://typeship.dev/docs/guides/add-to-your-repo). **Generated files are never edited by hand.** Wrap the client, add your own modules beside it, keep customizations in the project's config and spec patches so regeneration carries them. The package's `AGENTS.md` says this to coding agents too. See [Extend the client](https://typeship.dev/docs/guides/customize). **Every spec change becomes a pull request per language** with the API changes in the body; review and merge. Preview builds on spec PRs show the diff before the spec merges. See [Regeneration](https://typeship.dev/docs/projects/regeneration). ## What can go wrong | You see | Do | | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `A spec source is required` | pass `--spec-url`, or `--source` with `kind: repo`, `repo`, and `path` | | `plan_limit_reached` / `PLAN_LIMIT` on the second platform | the free plan runs one platform per project; add the rest on Pro, or start with the SDK alone | | the first pull request never arrives | the GitHub App is not installed on the destination repository; the project page says so | > **For AI agents:** Do steps 1 and 2; stop before merging a pull request unless the user asked you to. Installing the GitHub App is a browser step for the user; give them the link from the error. --- # Keep a generated package current Source: https://typeship.dev/docs/workflows/keep-a-package-current.md What regenerates when, how to force a run, how to read what changed, and how to handle a spec change that breaks the client. ## Quick reference ```bash typeship projects get # source, languages, platforms, auto_regen, destinations typeship projects generate # force a run now (URL-sourced projects) typeship projects list-generations # history with status, trigger, file counts typeship generations get # one run: meta, warnings, files (or files_index when large) typeship spec-versions list # every distinct spec the project generated from typeship projects update --auto-regen false # pause automatic regeneration ``` ## Why these commands **Regeneration is change-driven.** Auto-regen (on by default) compares the spec's hash with the last generation: a repository source regenerates on pushes that touch the spec, a URL source on a 30-minute poll. An unchanged spec is skipped. `projects generate` always runs. See [Regeneration](https://typeship.dev/docs/projects/regeneration). **A pull request per language is the unit of change.** Its body lists the API changes (new operations, changed types, removed fields) so review is about the API, not the diff. If a pull request cannot be opened (no destination, app not installed), the generation still records and the project page says why. **History is the audit trail.** `list-generations` and `spec-versions` tell you which spec produced which package, with warnings per run. Large generations return `files_omitted: true` and a `files_index`; fetch one file with `generations get-file --path

`. **When a spec change breaks the client**, fix it at the source: a [spec patch](https://typeship.dev/docs/projects/spec-patches) (rename, retype, remove at a JSON Pointer) is applied before every generation and survives, where an edit to the output does not. Preview builds on spec pull requests surface the break before the spec merges. > **For AI agents:** To know if a package is current: > > `projects get` > > and > > `list-generations` > > , compare the latest generation's spec version with > > `spec-versions list` > > . Never edit generated files; propose a spec patch. --- # Connect a generated MCP server to a client Source: https://typeship.dev/docs/workflows/connect-a-generated-mcp-server.md Your API's MCP server, generated by typeship: the local package, the hosted endpoint, and one command that registers either with every agent client on a machine. ## Quick reference ```bash # local server from the generated package (reads the CLI's saved credentials) acme login --token acme mcp install --all # Claude Code, Codex, VS Code, Windsurf, Gemini CLI, OpenCode, Zed, Claude Desktop acme mcp # print the entry and which clients were detected # hosted endpoint (Enterprise: typeship runs it at a stable URL) acme mcp install --all --url https://typeship.dev/mcp/ claude mcp add --transport http acme https://typeship.dev/mcp/ ``` `acme` stands for your generated CLI's name, derived from your API's title. ## Why these commands **The generated package ships the server.** `acme-mcp` speaks stdio and Streamable HTTP, exposes one tool per operation with typed input and output schemas plus `search_docs` and `read_docs`, and reads the same credentials `acme login` saves. See [MCP server](https://typeship.dev/docs/platforms/mcp). **`mcp install --all` writes every client at once.** It detects the clients on the machine and merges an entry into each one's config, preserving what is there: `.mcp.json` for Claude Code, `config.toml` for Codex, `.vscode/mcp.json`, and the rest. For a hosted endpoint the entry is the URL with the auth env var as a reference (`${ACME_TOKEN}`), never a literal key. Cursor is skipped until it speaks MCP 2026-07-28; `--cursor` writes it on request with that note. **The hosted endpoint is your API as a remote MCP server.** `https://typeship.dev/mcp/` runs the same tool surface with the caller's `Authorization` passed through to your API; when your spec declares OAuth, unauthenticated clients get the RFC 9728 challenge. See [Hosted endpoint](https://typeship.dev/docs/platforms/mcp#hosted-endpoint). **Verify in the client.** `acme doctor` reports which clients are detected and configured; in the client, list tools and call a read-only one. > **For AI agents:** Prefer > > `acme mcp install --all` > > over editing client config by hand. If the user wants a specific client, pass its flag. Never write a literal token into a config file. --- # Debug a spec that will not generate Source: https://typeship.dev/docs/workflows/debug-a-spec.md From spec_error or a wall of warnings to a clean generation: read the message, find it in the reference, fix it in the spec or with a patch, regenerate. ## Quick reference ```bash typeship generate run --spec '{"url":"..."}' --language typescript > /tmp/gen.json 2>/tmp/gen.err cat /tmp/gen.err # SPEC_INVALID envelope when the spec is unusable; the generator's message verbatim jq '.warnings' /tmp/gen.json # what was skipped or approximated typeship docs search "" # the entry in Errors and warnings, with the fix typeship docs read spec-compatibility # what maps to what ``` ## Why these commands **Two kinds of message.** An error (`spec_error` on the API, `SPEC_INVALID` in the CLI) means the document cannot be used as a spec at all: not YAML or JSON, no paths, an unsupported version. Warnings never block generation; they say what was left out. Every message is listed verbatim with its fix in [Errors and warnings](https://typeship.dev/docs/reference/errors-and-warnings). **Most fixes are one field.** A relative `servers[0].url` means every user passes `baseUrl`; an `operationId` missing or duplicated means a derived, uglier method name; a security scheme typeship cannot map means no auth option; a `oneOf` without a discriminator means a looser type. [Spec compatibility](https://typeship.dev/docs/reference/spec-compatibility) is the map from spec construct to generated shape. **Fix at the source, or patch.** Change the spec when you own it. When you do not, a [spec patch](https://typeship.dev/docs/projects/spec-patches) on the project sets, renames, or removes at a JSON Pointer before every generation and survives regeneration. `typeship projects update --spec-patches '[...]'` then `typeship projects generate `, and diff `.warnings`. **Large specs.** Past 100 operations the MCP server switches to meta tools (search, read, execute) so agents are not flooded; the SDK and CLI are unaffected. Anonymous and free generations stop at 25 operations, which is a cap, not a spec problem. > **For AI agents:** Quote the generator's message to the user exactly; it is written to be searched. Propose the smallest spec change or patch that clears it, regenerate, and show the warning count before and after. Do not hand-edit generated files to work around a spec problem. --- # Overview Source: https://typeship.dev/docs/typeship-api/api.md typeship's own HTTP API, for driving generation and projects from scripts, pipelines, and agents. Everything the console does, minus creating API keys. The typeship API is how you automate typeship: generate a package from a pipeline, create and configure projects in code, trigger regeneration, read generations and spec versions, and check usage. It is also how agents drive typeship. The [typeship SDK](https://typeship.dev/docs/sdks), [typeship CLI](https://typeship.dev/docs/cli), and [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp) are all generated from this API's spec by typeship itself, so they mirror it exactly. ```text https://typeship.dev/api/v1 ``` The full reference, generated from the OpenAPI spec with request and response schemas and a playground, is at [API reference](https://typeship.dev/docs/api). The spec itself is at [/openapi.yaml](https://typeship.dev/openapi.yaml). ## Resources | Resource | What it does | | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | `generate` | Run the generator on a spec. Returns files, stores nothing. | | `projects` | Create, configure, list, update, and delete projects. Trigger regeneration. List a project's generations and spec versions. | | `generations` | Read a generation, including its files, and fetch a single file. | | `spec_versions` | Read a spec version and its raw content. | | `account` | Read the account behind a key and set its defaults. | | `usage` | Hosted generation allowance, included endpoints, and who has been calling (by surface and agent harness, last 30 days). | | `api_keys` | List and revoke keys. | ## Conventions * JSON in and out, `snake_case` field names, ISO 8601 timestamps. * Every resource has an `object` field and a typed, prefixed id: `prj_`, `gen_`, `spec_`, `key_`. * Errors come in one envelope with a request id. See [Errors](https://typeship.dev/docs/typeship-api/api/errors). * Lists page with a cursor. See [Pagination](https://typeship.dev/docs/typeship-api/api/pagination). * Authenticate with an API key. See [Authentication](https://typeship.dev/docs/typeship-api/api/authentication). ## A first call Generate a Python package from a public spec: ```bash curl -s https://typeship.dev/api/v1/generate \ -H "Authorization: Bearer $TYPESHIP_TOKEN" \ -H "Content-Type: application/json" \ -d '{"spec":{"url":"https://api.acme.example.com/openapi.json"},"platforms":["sdk"],"language":"python"}' ``` The response carries every file, the warnings, and metadata about what was generated: ```json { "files": [{ "path": "README.md", "content": "..." }, { "path": "acme/__init__.py", "content": "..." }], "warnings": [], "meta": { "title": "Acme API", "version": "2.3.0", "spec_format": "openapi", "package_name": "acme", "client_name": "AcmeClient", "operation_count": 6, "file_count": 18 } } ``` Ad hoc generation is capped at the first 25 operations and keeps nothing. To keep a package current, create a project: ```bash curl -s https://typeship.dev/api/v1/projects \ -H "Authorization: Bearer ak_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme API", "spec_url": "https://api.acme.example.com/openapi.json", "platforms": ["sdk", "cli", "mcp"], "languages": ["typescript", "python"], "destinations": { "typescript": { "repo": "acme/acme-node" }, "python": { "repo": "acme/acme-python" } }, "auto_regen": true }' ``` Then regenerate on demand and read the result: ```bash curl -s -X POST https://typeship.dev/api/v1/projects/prj_.../generations \ -H "Authorization: Bearer ak_..." ``` The response is a list with one generation per language. See [Generate in CI](https://typeship.dev/docs/guides/ci) for a full pipeline. ## What the API cannot do * **Create an API key.** Keys are created in the console only. A leaked key that can mint keys is a leaked account. * **Change the plan.** Billing runs through the console. * **Regenerate a repository-sourced project on demand.** Those regenerate on push. Use **generate now** in the console to force one. --- # Authentication Source: https://typeship.dev/docs/typeship-api/api/authentication.md API keys for the typeship API: how to create one, how to send it, and how it is stored. The typeship API authenticates with API keys. One key identifies one account. The same key works for the [typeship SDK](https://typeship.dev/docs/sdks), the [typeship CLI](https://typeship.dev/docs/cli), and the [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp). ## Create a key In the console under **api keys**, name the key and choose **create key**. The full key is shown once. Copy it then; afterwards the console shows the last four characters to tell keys apart. Or let the [typeship CLI](https://typeship.dev/docs/cli) ask for one: `typeship login` opens the console's approval page, you approve `typeship CLI on ` for the active organization, and the API mints the key straight to that machine. The request carries a PKCE-style challenge; the key is handed only to the typeship CLI that proves the matching verifier, once, and never appears in the browser. Such keys are listed and revoked like any other. Keys look like this: ```text ak_7Q2KD4MZX9P1VN6TBH8CRW3JSFY5LAGE ``` Name keys after where they live (`ci`, `laptop`, `agent`) so revoking one later is a decision, not a guess. > **For AI agents:** Run > > `typeship login --no-browser` > > : it prints an approval link (also as a JSON event on stderr), you hand the link to the user, they approve once in the console, and the typeship CLI stores the key it is given, named after the machine. No key crosses the conversation. Alternatively the user creates one at > > [/console/keys](https://typeship.dev/console/keys) > > and exports it as > > `TYPESHIP_TOKEN` > > . Until then, > > `POST /v1/generate` > > and > > `typeship generate run` > > work without a key. ## Send it Every authenticated request carries the key as a bearer token: ```bash curl https://typeship.dev/api/v1/me \ -H "Authorization: Bearer ak_..." ``` A missing or invalid key returns `401` with the code `unauthorized`. A browser session is not a credential for this API. Only keys work. ## OAuth access tokens An OAuth access token from a sign-in through typeship's MCP server (`/mcp-oauth`, or any OAuth client registered with typeship's authorization server) is accepted wherever a key is: `Authorization: Bearer `. It acts as the user who consented, in the first organization they belong to; `X-Typeship-Org: org_...` picks another they are a member of. Tokens expire on the authorization server's schedule and clients refresh them; revoking the client's grant ends access. ## Without a key `POST /v1/generate` is the one operation that works anonymously. Leave the `Authorization` header off and it behaves like the [generator on the homepage](https://typeship.dev/): the first 25 operations of the spec, rate limited per IP address, nothing stored. The response carries a `limits` object naming what was held back and where to sign up, and, for a spec given by URL, a `claim.url`: a link that, once a person signs in, turns that run into a project in their organization with the same spec, language, platforms, and config (seven days). That is the front door for a script or an agent that has a spec and no account yet. A key that is present but invalid is a `401`, never a silent downgrade to anonymous. Everything else needs a key. ## Rotate and revoke List keys with `GET /v1/api_keys` and revoke one with `DELETE /v1/api_keys/{id}` (`typeship api-keys list`, `typeship api-keys revoke --force`), or use **revoke** in the console. Revocation takes effect within thirty seconds. Revoked keys stay listed with `revoked: true` for the audit trail. Rotating means creating a new key in the console, moving it into place, and revoking the old one. Each key records when it was last used, so a key that has gone quiet is easy to spot before you revoke it. ## Scope Keys have the full permissions of the account, no expiry, and no scopes. Treat them like passwords: environment variables and secret stores, never source control. The one thing a key cannot do is create another key. ## In the tooling | | Where the key goes | | ------------------- | ----------------------------------------------------------- | | typeship SDK | `new TypeshipClient({ bearerToken })` | | typeship CLI | `TYPESHIP_TOKEN`, `--token`, or `typeship login` | | typeship MCP server | `TYPESHIP_TOKEN`, or the credentials `typeship login` saved | | curl | `Authorization: Bearer ak_...` | --- # Errors Source: https://typeship.dev/docs/typeship-api/api/errors.md One error envelope, a fixed set of codes, and a request id on every response. Every error from the typeship API uses the same envelope: ```json { "errors": [ { "code": "invalid_request", "message": "name is required." } ], "request_id": "req_k3j2h8f0a1b2c3d4" } ``` `errors` is always an array, even for a single error. `request_id` also arrives on every response as the `x-request-id` header. Quote it when reporting a problem. ## Codes | Code | Status | Retry | When | | -------------------- | ---------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request` | 400 | No | Malformed JSON, a failed field validation, a bad cursor, or an operation that does not apply (regenerating a repository-sourced project on demand). The message names the field. | | `unauthorized` | 401 | No | Missing, malformed, or revoked API key. Fix the credential first. | | `plan_limit_reached` | 402 | No | The account has used its included hosted generations. The response names where to lift it. | | `not_found` | 404 | No | The resource does not exist or belongs to another account. The two are indistinguishable on purpose. | | `payload_too_large` | 413 | No | A spec over 10MB, inline or fetched. | | `spec_error` | 422 | No | The document cannot be used as a spec at all. The message is the generator's, quoted verbatim. Warnings never cause this. See [Errors and warnings](https://typeship.dev/docs/reference/errors-and-warnings). | | `fetch_error` | 400 or 502 | 502 only | The spec URL was invalid (400) or could not be fetched (502): unreachable, timed out after 15 seconds, or answered with an error status. | | `rate_limited` | 429 | After `Retry-After` | Too many requests. A `Retry-After` header says how long to wait. See [Rate limits](https://typeship.dev/docs/typeship-api/api/rate-limits). | | `internal_error` | 500 | Once, with backoff | The generator hit an unexpected condition. Not a problem with your spec. | ## Agent guidance Read `errors[0].code`, never the message, and act on the table above. Three cases deserve care: * **`plan_limit_reached` and anonymous caps are not failures to retry.** An anonymous `POST /v1/generate` succeeds with a `limits` object (`max_operations`, `omitted_operations`, `reason`, `signup_url`, `upgrade_url`); a 402 carries the same idea for hosted generation. Tell the user what was left out and where the cap lifts; do not loop on the same call. * **`rate_limited` says how long.** Wait the `Retry-After` seconds once. Anonymous calls also carry `X-RateLimit-Limit` and `X-RateLimit-Remaining` on every response, so pace before the wall, not after. * **Large results are paged and indexed.** A generation over the size cap returns `files_omitted: true` with a `files_index`; fetch the files you need with `GET /v1/generations/{id}/file?path=...` instead of asking for the whole generation again. Lists return `has_more` and `next_cursor`. The typeship CLI turns all of this into its own envelope with stable codes and `next_steps` (below); the [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp) returns the same bodies as tool errors. ## In the typeship SDK Nothing throws. Every call returns a discriminated result, and the error side is a typed union of the documented errors for that operation. The envelope is the typed `body`: ```ts const result = await client.projects.create({ name: "" }); if (!result.ok) { result.error.status; // 400 result.error.body.errors[0]; // { code: "invalid_request", message: "name can't be empty." } result.response?.requestId; // "req_..." } ``` `PaymentRequiredError`, `NotFoundError`, `UnprocessableEntityError`, and the rest are exported by name. `unwrap(result)` throws them instead. See [Results and errors](https://typeship.dev/docs/platforms/sdk#results-and-errors) for the pattern, which is the same one every SDK typeship generates uses. ## In the typeship CLI Errors print one JSON envelope on stderr, `{status, issues: [{code, message}], docs_url, next_steps, detail}`, and exit `1`; the API's envelope above rides along as `detail.body`, and the typeship CLI's `issues[].code` is derived from the status (`NO_AUTH`, `PLAN_LIMIT`, `RATE_LIMITED`, `SPEC_INVALID`, and so on). Usage mistakes exit `2` before any request is made. See [Output and exit codes](https://typeship.dev/docs/platforms/cli#output-and-exit-codes). --- # Pagination Source: https://typeship.dev/docs/typeship-api/api/pagination.md List endpoints page with a cursor. Ask for a limit, follow next_cursor until it is null. List endpoints on the typeship API return one page at a time and a cursor for the next: ```bash curl -s "https://typeship.dev/api/v1/projects?limit=20" \ -H "Authorization: Bearer ak_..." ``` ```json { "data": [ { "id": "prj_...", "object": "project", "...": "..." } ], "next_cursor": "MjAyNi0wOC0xOFQxMjowMDowMC4wMDBafHByal8..." } ``` Pass `next_cursor` back as `cursor` for the following page. When `next_cursor` is `null`, you have everything. | Parameter | Meaning | | --------- | ------------------------------------------------- | | `limit` | Items per page. Default 20, maximum 100. | | `cursor` | The `next_cursor` from the previous page. Opaque. | Results are newest first. A cursor that was not issued by the API returns `400` with `Malformed cursor.` Paginated operations: `GET /v1/projects`, `GET /v1/api_keys`, `GET /v1/projects/{id}/generations`, and `GET /v1/projects/{id}/spec_versions`. ## In the typeship SDK Paginated methods return a `PagePromise`. Iterate it to walk every item, or await it for one page: ```ts for await (const project of client.projects.list()) { console.log(project.name); } const page = await client.projects.list({ limit: 50 }); if (page.ok) { page.data.items; page.data.hasNextPage(); await page.data.getNextPage(); } ``` ## In the typeship CLI `--all` walks every page and prints one item per line: ```bash typeship projects list --all | jq -r '.id' ``` --- # Rate limits Source: https://typeship.dev/docs/typeship-api/api/rate-limits.md Limits on the typeship API, what a 429 looks like, and how the generated tooling handles it for you. | Scope | Limit | | ------------------------------------------ | --------------------------------------------------------- | | Anonymous generation (`POST /v1/generate`) | 20 requests per minute per IP address | | Authenticated endpoints | 60 requests per minute per API key | | Hosted generations | Per plan. See [Plans and limits](https://typeship.dev/docs/reference/limits). | Anonymous generation also reports its budget as it goes: `X-RateLimit-Limit` and `X-RateLimit-Remaining` ride on every anonymous response, so a client can pace itself before it hits the wall. Limits are counted in one shared store, so they hold across regions and instances. Over the limit, the API returns `429` with the code `rate_limited` and a `Retry-After` header in seconds: ```http HTTP/1.1 429 Too Many Requests Retry-After: 42 Content-Type: application/json { "errors": [{ "code": "rate_limited", "message": "Too many requests. Retry after 42s." }], "request_id": "req_..." } ``` ## What the tooling does The [typeship SDK](https://typeship.dev/docs/sdks) and [typeship CLI](https://typeship.dev/docs/cli) retry `429` automatically with backoff, honoring `Retry-After`, up to two retries by default. Most scripts never see the status. If yours does, back off and retry after the header's value. ## Generation is the expensive call Generation runs the whole engine on your spec, and large specs take seconds. If you are generating in a loop, you almost certainly want a project instead. Projects regenerate when the spec changes and not otherwise. See [Regeneration](https://typeship.dev/docs/projects/regeneration). --- # Overview Source: https://typeship.dev/docs/sdks.md typeship's own SDKs for its API, in TypeScript, Python, and Go. Generated by typeship from its own spec, so each is also a sample of what your users get. typeship drives its own API through the packages it generates for itself. Each is built from [the same OpenAPI spec](https://typeship.dev/openapi.yaml) by the same engine that generates your packages, and each has the shape every SDK typeship generates in that language: zero runtime dependencies, typed payloads and errors, auto-pagination, retries, hooks, and validation. If you want to see what your users will get before you generate anything, read one of these. | Language | Package | Install | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | [TypeScript](https://typeship.dev/docs/sdks/typescript) | `typeship-ax` on npm, which also carries the [typeship CLI](https://typeship.dev/docs/cli) and the [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp) | `npm install typeship-ax` | | [Python](https://typeship.dev/docs/sdks/python) | `typeship` on PyPI | `pip install typeship` | | [Go](https://typeship.dev/docs/sdks/go) | `github.com/typeship-ax/go` | `go get github.com/typeship-ax/go` | All three take an API key from the console under **api keys**, call the same seventeen operations, and read the same `snake_case` payloads. What differs is the idiom: TypeScript returns `ApiResult`, Python raises, Go returns `(T, error)`. The [SDK platform pages](https://typeship.dev/docs/platforms/sdk) describe each idiom in full; the pages here cover what is specific to typeship's operations. The packages live in the typeship repository under `packages/typeship-sdk`, `packages/typeship-python`, and `packages/typeship-go`, regenerated from the spec on every change. --- # typeship CLI Source: https://typeship.dev/docs/cli.md Drive typeship from a terminal or a pipeline: generate packages, manage projects, trigger regeneration, and read results as JSON. The `typeship` command is the CLI for typeship's own API. It ships in the `typeship-ax` npm package and is generated by typeship from its own spec, so it behaves exactly like the CLIs typeship generates for your API: flags from the spec, JSON on stdout, exit codes `0`, `1`, and `2`, `login`, `config`, `docs`, `completion`, and the rest. Everything on the [CLI](https://typeship.dev/docs/platforms/cli) page applies. This page covers what is specific to typeship's commands. ## Install ```bash npm install -g typeship-ax typeship --version ``` The package is `typeship-ax`; the command it installs is `typeship`. The bare name was taken on npm, and a registry name is claimed against every package in the world while a command is only claimed against your PATH, so the two differ here and only here. Everything else is `typeship`: the command, `TYPESHIP_TOKEN`, `~/.config/typeship/`, and the `typeship-mcp` server. `npx -y typeship-ax@latest ` runs it without installing. ## Log in ```bash typeship login # opens the console; approve once, a key is minted for this machine typeship login --no-browser # prints the approval link instead (headless, or an agent relaying it to you) typeship login --token ak_... # a key you already have echo "$TYPESHIP_TOKEN" | typeship login --with-token # from a secret, in scripts typeship whoami # GET /v1/me ``` The first form needs no key in hand: the console page at `/cli-auth` asks you to approve `typeship CLI on ` (and, if you belong to several organizations, which one the key is for), and the typeship CLI stores the key it gets back, named after the machine so you can revoke it under **api keys** later. `typeship logout` revokes a key minted this way as it removes it; a key you pasted is left alone, since it was not the typeship CLI's to end. Credentials are stored in `~/.config/typeship/credentials.json`. `TYPESHIP_TOKEN` and `--token` win over stored credentials, so CI needs no login step. ## Generate a package Once logged in (or with `TYPESHIP_TOKEN` set): ```bash typeship generate run \ --spec '{"url":"https://api.acme.example.com/openapi.json"}' \ --platforms '["sdk","cli","mcp"]' \ --language typescript > result.json ``` `--spec` accepts `{"url": ...}` or `{"inline": ""}`. `--language` is `typescript`, `python`, or `go`. `--config '{...}'` takes the same [config](https://typeship.dev/docs/projects/config) object as a project. Output is the generation result: `files`, `warnings`, and `meta`. ## Manage projects ```bash typeship projects list --all typeship projects create --name "Acme API" \ --spec-url https://api.acme.example.com/openapi.json \ --platforms '["sdk","cli"]' \ --languages '["typescript","python"]' \ --auto-regen true typeship projects get prj_... typeship projects update prj_... --spec-patches '[{"op":"set","path":"/info/title","value":"Acme"}]' typeship projects delete prj_... ``` Object-valued fields (`--destinations`, `--package-names`, `--config`) take JSON. `--data ''` supplies the whole body when that is easier. ## Regenerate and read results ```bash typeship projects generate prj_... # one generation per language, with files typeship projects list-generations prj_... --all # history, newest first typeship generations get gen_... # one generation with files typeship generations get-file gen_... --path src/index.ts # a single file, for large outputs typeship spec-versions list prj_... --all typeship spec-versions get-content spec_... # the raw spec a generation came from ``` ## Account and usage ```bash typeship usage retrieve typeship account me typeship api-keys list typeship api-keys revoke key_... --force ``` `usage retrieve` includes `requests`: who called the API in the last 30 days by surface (`cli`, `mcp`, `sdk`, `http`) and by agent harness, from the User-Agent the typeship CLI and MCP server send (`typeship-cli/1.0.0 (typeship; harness=claude-code; agent)`). It carries no secrets and nothing else is collected. ## For agents One line sets a machine up. With no key anywhere, `init` starts the browser approval itself (an agent prints the link for you and waits), so the first command on a new machine is also the only one: ```bash typeship init --all # approve in the browser; or -k ak_... with a key in hand ``` It stores the key, installs the [typeship skills](https://github.com/typeship-ax/skills), writes the [hosted MCP endpoint](https://typeship.dev/docs/typeship-api/mcp) into every agent client on the machine (the key as an env reference, never a literal), and adds a `typeship` block to `AGENTS.md`. Then `typeship agent-guide --format json` explains the conventions, `typeship auth check --live` confirms the key, and `typeship doctor` checks the rest. Errors are one JSON envelope on stderr with a stable `issues[].code` and `next_steps`; deletes need `--force`; `--mode agent` (or no terminal) turns every prompt into an envelope. `typeship generate run ... --out sdk/` writes a generated package straight into a directory. `typeship docs search "spec patches"` searches these docs from the terminal. The full contract is on the [CLI platform page](https://typeship.dev/docs/platforms/cli#agent-mode); the runbook an agent reads first is [typeship.dev/agents.md](https://typeship.dev/agents.md). --- # MCP server Source: https://typeship.dev/docs/typeship-api/mcp.md Let a coding agent drive typeship: generate packages, create and configure projects, trigger regeneration, and read results, all as tools. Hosted at typeship.dev/mcp, or local from the typeship package. typeship's MCP server exposes typeship's own API as tools: one per operation, typed input schemas, `search_docs` and `read_docs` over these docs. It is generated by typeship from its own spec, so it behaves like the MCP servers typeship generates for your API. Everything on the [MCP server](https://typeship.dev/docs/platforms/mcp) platform page applies. There are two ways to run it. The hosted endpoint needs nothing installed: `https://typeship.dev/mcp-oauth` signs you in from the client, `https://typeship.dev/mcp` takes an API key. The local server, `typeship-mcp`, ships in the `typeship-ax` npm package for clients that want stdio (`npx -y typeship-ax typeship-mcp` runs it without installing). Both are listed in the [official MCP registry](https://registry.modelcontextprotocol.io/v0/servers?search=typeship) as `dev.typeship/typeship`, so a client that browses the registry can add typeship by name. ## Hosted endpoint Streamable HTTP, MCP 2026-07-28, at `https://typeship.dev/mcp-oauth`. Connect, and the client opens a browser: sign in once, and the connection acts as you. No key is created, copied, or pasted. ```bash claude mcp add --transport http typeship https://typeship.dev/mcp-oauth ``` Codex CLI: `codex mcp add typeship --url https://typeship.dev/mcp-oauth`, then `codex mcp login typeship`. VS Code, one click: [Add typeship to VS Code](vscode:mcp/install?%7B%22name%22%3A%22typeship%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Ftypeship.dev%2Fmcp-oauth%22%7D). Claude Desktop and claude.ai: add `https://typeship.dev/mcp-oauth` as a custom connector (Settings, Connectors). Any client that reads a JSON config (`.mcp.json` for Claude Code, `.vscode/mcp.json` for VS Code, and most others): ```json { "mcpServers": { "typeship": { "type": "http", "url": "https://typeship.dev/mcp-oauth" } } } ``` Under the hood: an unauthenticated request is answered with an RFC 9728 challenge naming the endpoint's [metadata](https://typeship.dev/.well-known/oauth-protected-resource/mcp-oauth); the authorization server is typeship's Clerk instance, which supports dynamic client registration and PKCE, so a current MCP client completes the flow with nothing configured in advance. The access token it comes back with is accepted by the [API](https://typeship.dev/docs/typeship-api/api) directly, too. No tools are gated by scope: a signed-in connection can do what a key can. If you belong to several organizations, choose the one signed-in agents act in under **API keys → Signed-in agents** in the console (or send `X-Typeship-Org: org_...` with each request). Until you do, tools that need an organization answer `organization_required` and name the choice. A member of one organization has nothing to choose. ### With a key, for CI and headless agents The same server at `https://typeship.dev/mcp` takes an API key instead, carried as an environment reference so it never lands in a config file. This is the form for pipelines, and for an agent with no browser to hand: ```bash export TYPESHIP_TOKEN=ak_... claude mcp add --transport http typeship https://typeship.dev/mcp \ --header 'Authorization: Bearer ${TYPESHIP_TOKEN}' ``` The single quotes matter: Claude Code stores the reference and expands `${TYPESHIP_TOKEN}` when it connects, so the key stays in your environment. Codex: `codex mcp add typeship --url https://typeship.dev/mcp --bearer-token-env-var TYPESHIP_TOKEN`. `typeship mcp install --all` writes this entry into every client on the machine (`--vscode`, `--windsurf`, and the rest for one). As JSON: ```json { "mcpServers": { "typeship": { "type": "http", "url": "https://typeship.dev/mcp", "headers": { "Authorization": "Bearer ${TYPESHIP_TOKEN}" } } } } ``` Requests through the hosted endpoint count against the same limits as direct API calls (see [Rate limits](https://typeship.dev/docs/typeship-api/api/rate-limits)); tool calls themselves are capped at 120 per minute per caller. The endpoint speaks the current MCP protocol only, like every server typeship generates. A client that still needs the legacy `initialize` handshake gets an error naming the version it must support. ### Without a key Connect with no `Authorization` header and the server still answers, with the docs tools (`search_docs`, `read_docs`, `query_docs`, `submit_docs_feedback`) and `generate_run`. `generate_run` behaves like the [generator on the homepage](https://typeship.dev/): the first 25 operations of a spec, rate limited per address, nothing stored. That is enough for an agent that has a spec and no typeship account to produce a package. Every other tool answers with a message naming the key it needs and where to create one, and never retries anything upstream. Requests through the hosted endpoint count against the same limits as direct API calls (see [Rate limits](https://typeship.dev/docs/typeship-api/api/rate-limits)); tool calls themselves are capped at 120 per minute per caller. ## Local server ```bash npm install -g typeship-ax # or: npx -y typeship-ax typeship-mcp typeship login --token ak_... typeship mcp --claude # ./.mcp.json typeship mcp --cursor # ./.cursor/mcp.json typeship mcp --claude-desktop # Claude Desktop ``` The local server reads the credentials `typeship login` saved, or `TYPESHIP_TOKEN` from its environment. No token goes into the config file. See [Connect MCP clients](https://typeship.dev/docs/guides/mcp-clients) for manual configuration. ## Tools One per operation, named `resource_method`: | Tool | Does | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `generate_run` | Generate a package from a spec URL or inline text. Nothing is stored. Works without a key. | | `projects_list`, `projects_create`, `projects_get`, `projects_update`, `projects_delete` | Manage projects, including languages, destinations, spec patches, config, and hosted toggles. | | `projects_generate` | Regenerate a project and return every language's files. | | `projects_list_generations`, `generations_get`, `generations_get_file` | Read history, a generation, or one file. | | `spec_versions_list`, `spec_versions_get`, `spec_versions_get_content` | The spec audit trail. | | `account_me`, `account_update`, `usage_retrieve` | Account, defaults, allowance. | | `api_keys_list`, `api_keys_revoke` | Key hygiene. | | `search_docs`, `read_docs` | Search and read these docs and the API reference from inside the session. Work without a key. | | `query_docs` | Grep every docs page and both references with a regular expression; matching lines with their page and context. Works without a key. | | `submit_docs_feedback` | Tell the typeship team a page is wrong, missing, or unclear. Works without a key. | ## What an agent can do with it * "Generate a Python SDK for this OpenAPI URL and save the files under `sdk/`." * "Create a typeship project for our API with TypeScript and Go, pointed at these two repos, and turn on auto-regen." * "Add a spec patch that retypes every `account_id` to string, then regenerate and tell me what changed." * "How many hosted generations do we have left this month?" Because `read_docs` returns these docs, an agent can also answer questions about how typeship works without leaving the session. ## Not available as tools Creating an API key. That is console-only, on purpose. --- # Spec compatibility Source: https://typeship.dev/docs/reference/spec-compatibility.md What typeship accepts: Swagger 2.0, OpenAPI 3.0 and 3.1, and GraphQL, and how each construct maps to the generated packages. typeship accepts Swagger 2.0, OpenAPI 3.0.x, and OpenAPI 3.1.x as JSON or YAML, up to 10MB, plus GraphQL schemas as SDL, introspection JSON, or an introspectable endpoint URL. The rule throughout: generation fails only for a document that is not a spec at all. Everything else generates, and anything skipped or approximated is reported as a warning on the result. ## Swagger 2.0 2.0 documents are normalized to OpenAPI 3.0 before generation. The converter covers the constructs that appear in real specs: * `host`, `basePath`, and `schemes` become the server URL. https is preferred when listed. * `definitions` become `components.schemas`. `$ref` pointers are rewritten to their 3.0 locations. * `body` parameters become request bodies. `formData` parameters become a form-encoded body, or multipart when there are file uploads. * `consumes` and `produces` become content types on request and response bodies. * `securityDefinitions` become security schemes. Basic, apiKey, and oauth2 are mapped. Unsupported types are skipped with a warning. Known limitation: shared parameters in the top-level `parameters` section that are `body` or `formData` parameters cannot be converted in place and are skipped with a warning. Inline them in each operation for full fidelity. ## References Only local references (`#/components/...`) are resolved. External references to other files or URLs are not fetched. The referenced schema becomes `unknown` and a warning is reported. Dangling local references are treated the same way. Bundle multi-file specs into one document before generating. ## GET and HEAD request bodies Request bodies declared on GET or HEAD operations are dropped, with a warning listing the affected operations. Intermediaries do not reliably transmit GET bodies, so the SDK refuses to expose them. ## Server URLs * The first entry in `servers` becomes the client's default base URL. Server variables are substituted with their declared defaults. * Two or more servers export named environments, taken from each server's description. * A relative server URL (`/v1`) or a variable without a default cannot produce a usable base URL. The client is still generated, but the base URL becomes a required constructor option and a warning says so. ## Authentication | Scheme in the spec | Client option | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | HTTP bearer | `bearerToken` | | HTTP basic | `basicAuth` | | API key in a header or query | `apiKey`, or one option per key when there are several | | OAuth2 and OpenID Connect | `bearerToken` for a token you obtained elsewhere, plus `clientCredentials` for the client credentials grant against the spec's token URL | | Cookie parameters | Dropped | | Anything else | Warning, no option | When OAuth2 is declared, the generated CLI also gains a device-flow `login` once a client id is configured. See [login, logout, whoami](https://typeship.dev/docs/platforms/cli#login-logout-whoami). ## Webhooks The `webhooks` section (3.1) and the `x-webhooks` convention (3.0) generate typed events and a verifying parser. Each webhook needs a JSON request body schema. See [Webhooks](https://typeship.dev/docs/guides/webhooks). ## Bodies and responses | Content type | Generated as | | ----------------------------------- | ---------------------------------------------------- | | `application/json` | Typed body, JSON encoded | | `application/x-www-form-urlencoded` | Typed body, deep bracket encoded | | `multipart/form-data` | SDK-only. Not in the CLI, MCP server, Python, or Go. | | Binary bodies | SDK-only. Same exclusions. | | `text/event-stream` responses | A stream. SDK-only. | | Other `text/*` responses | A string | | Other responses | Raw bytes | ## Pagination Four styles are detected from parameter and response names. When detection guesses wrong, pin the rule per operation. See [Pagination](https://typeship.dev/docs/platforms/sdk#pagination) and [Pagination rules](https://typeship.dev/docs/projects/config#pagination-rules). ## GraphQL GraphQL schemas generate the same TypeScript package as OpenAPI specs, on the same runtime. Queries become methods on `client.query`, mutations on `client.mutation`. Object types carry an optional `__typename` literal. Unions, and interfaces with implementers, become a union of their concrete types, discriminated by `__typename`. Subscriptions are skipped with a warning. Custom scalars map to `unknown`. Operation documents are not an input. The endpoint and auth scheme, which a schema cannot declare, come from [config](https://typeship.dev/docs/projects/config#graphql-schemas) and default to the URL the schema was fetched from and a bearer token. See [Generate from GraphQL](https://typeship.dev/docs/guides/graphql). ## When generation fails Hard failures are reserved for documents that are not a spec at all: empty input, unparseable JSON or YAML, a top level that is not an object, no `openapi` or `swagger` field, an unsupported version, no `paths`, no Query or Mutation fields, or more than 10MB. [Errors and warnings](https://typeship.dev/docs/reference/errors-and-warnings) lists every message with what to do about it. --- # Errors and warnings Source: https://typeship.dev/docs/reference/errors-and-warnings.md Every message the generator can produce, quoted verbatim, with what to do about it. Errors stop generation. Warnings never do. Generation fails hard only when the document cannot be used as a spec at all. Everything else generates, with warnings describing what was skipped or approximated. Messages are quoted exactly. A trailing `...` stands for details filled in from your input. ## Errors These stop generation. On the typeship API they arrive as `422` with the code `spec_error`. In the console they appear where the generation would have. | Error | What to do | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `The spec is empty.` | The input was blank. Paste the spec text, or check that the URL returns the document body. | | `This looks like JSON but failed to parse: ...` | The input starts with `{` or `[` but is not valid JSON. The rest of the message is the parser error. | | `Failed to parse as YAML: ...` | Invalid YAML. The rest of the message is the parser error. | | `The document parsed, but it isn't an object. An OpenAPI spec must be a JSON/YAML object with an \`openapi\` or \`swagger\` field.\` | The top level is an array or a scalar. Paste the spec document itself, not a fragment. | | `No \`openapi\` or \`swagger\` version field found. Is this an OpenAPI document?\` | The document is an object without a version field. Usually something other than the spec was pasted, such as an API response or a JSON Schema. | | `Unsupported Swagger version "...". Expected 2.0.` | Only Swagger 2.0 is converted. | | `Unsupported OpenAPI version "...". Supported: 2.0, 3.0.x, 3.1.x.` | Re-export the spec as a supported version. | | `The spec has no \`paths\` object, so there is nothing to generate.\` | A components-only document has no operations. Point at the full spec. | | `Failed to parse as GraphQL SDL: ...` | The GraphQL schema did not parse. The rest of the message is the parser error. | | `The GraphQL schema has no Query or Mutation fields, so there is nothing to generate.` | Add root fields, or point at the full schema. | | `Spec is larger than the 10MB limit.` | The hard size cap, for pasted and fetched specs alike. Trim examples or descriptions. | | `That doesn't look like a valid URL.` | URL sources need an absolute URL with a scheme. | | `Only http(s) URLs are supported.` | Other schemes are rejected. Serve the spec over http(s) or paste it. | | `Couldn't reach that URL. If the spec is behind auth, paste it instead.` | The server-side fetch failed or exceeded 15 seconds. typeship sends no credentials, so specs behind auth must be pasted. | | `The URL responded with HTTP . If the spec is behind auth, paste it instead.` | A `401` or `403` means the spec needs auth. A `404` usually means the URL points at documentation rather than the raw spec. | | `Malformed request.` | The request body sent to the generate endpoint was invalid. The console never produces this. It appears when calling the API with a wrong shape. | | `The generator hit an unexpected condition in this spec. This is a typeship bug, not a problem with your spec.` | Nothing to fix on your side. Retrying with a reduced spec narrows down which construct triggers it. | ## Warnings Warnings never block generation. They are attached to the result, shown in the console, and listed in regeneration pull requests. Counts, names, and operation lists are filled in from your spec. | Warning | Meaning and fix | | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Dropped request bodies declared on N GET/HEAD operations (...). GET bodies aren't reliably transmitted, so the SDK doesn't expose them.` | The listed operations are generated without their request body. Move the fields to query parameters, or change the operation to POST. | | `External $ref "..." is not supported. Treated as unknown.` | References to other files or URLs are not fetched. The referenced schema becomes `unknown`. Bundle multi-file specs into one document. | | `$ref "..." points nowhere. Treated as unknown.` | A dangling local reference. Fix the pointer. | | `Server URL "..." is relative. Pass \`baseUrl\` when constructing the client.\` | The spec has no usable absolute server URL. The base URL becomes a required constructor option, and the CLI and MCP server need `--base-url` or the environment variable. Also reported as `Server URL "..." has variables without defaults. Pass \`baseUrl\` when constructing the client.\` | | `N operations with binary/multipart bodies or streaming responses are SDK-only, not exposed in the CLI/MCP server (...).` | File-upload payloads cannot ride flags or tool arguments, and event streams have no CLI or MCP rendering. Call these from code. | | `The Python SDK does not support ... yet, so N operations were left out: ...` | Reserved for a shape an emitter cannot express. Every body kind (JSON, form, multipart, binary, text), streaming, and GraphQL ship in all three languages today, so this does not currently fire; if it ever does, it names the reason and each operation, so a skipped endpoint is never silent. | | `Security scheme "..." (...) isn't mapped to a client option.` | An auth type outside bearer, basic, apiKey, OAuth2, and OpenID Connect. Pass credentials through `defaultHeaders` or the `fetch` option. | | `Security scheme "..." has unsupported type "...". Skipped.` | Swagger 2.0 only. Same fix. | | `Shared parameter "..." is a body parameter. Inline it in each operation for full fidelity. Skipped.` | Swagger 2.0 only. Shared body and formData parameters cannot be converted in place. | | `Webhook "..." declares no JSON request body schema. Skipped.` | Give the webhook a JSON request body schema and it becomes a typed event. | | `globals: "..." matches no query or header parameter on any operation (path parameters aren't supported). Ignored.` | A [global parameter](https://typeship.dev/docs/projects/config#global-parameters) name that appears nowhere. Check the wire name. | | `retries.operations: "..." matches no operation (use an operationId or "METHOD /path"). Ignored.` | A retry or pagination key that matches nothing. Same for `pagination:`. | | `pagination.: itemsField is required. Falling back to detection.` | A malformed [pagination rule](https://typeship.dev/docs/projects/config#pagination-rules). The message names what is missing or wrong. Detection is used until the rule is fixed. | | `Spec patch matched nothing, not applied: ()` | A [spec patch](https://typeship.dev/docs/projects/spec-patches) whose target no longer exists. Remove or update it. Similar messages report an `append` on a non-array, a `rename` on a non-key, and a `rename` whose destination exists. | | `N spec patch(es) skipped: patches apply to OpenAPI documents, not GraphQL schemas.` | Patches are configured on a GraphQL project. | | `Output includes the first 25 operations; N more in this spec were not generated.` | Free generation covers the first 25 operations of a spec, in spec order. Paid plans lift the cap. | | `Python output is the SDK only; cli, mcp are generated from the TypeScript target.` | The CLI or MCP server was requested with a Python or Go generation. They come from the TypeScript package. | ## Limits * Specs up to 10MB, pasted, uploaded, or fetched from a URL. * A pasted spec travels in the request body and can be rejected by the platform's request cap before it reaches the generator. The homepage then shows `That spec is bigger than the hosted request limit (~4.5MB).` Use a URL instead. The server fetches it directly and only the 10MB limit applies. * URL fetches follow redirects and time out after 15 seconds. * Generated output over 3.5MB is delivered file by file on the API. See [Plans and limits](https://typeship.dev/docs/reference/limits). --- # Plans and limits Source: https://typeship.dev/docs/reference/limits.md What each plan includes, what is metered, and the hard limits that apply everywhere. Full pricing is on the [pricing page](https://typeship.dev/pricing). This page is the reference for what gates on which plan and the limits that apply regardless of plan. ## Plans Pro is priced per platform, per linked project, per month, with unlimited generations while linked. A platform is one generated target for a project: an SDK in one language (TypeScript, Python, and Go each count as one), the CLI, or the MCP server. A TypeScript SDK, a Python SDK, and a CLI are three platforms. Each language a project generates is its own package, its own pull request, and its own hosted generation. Enterprise covers large APIs, many projects, the hosted MCP endpoint, and contracts. | | Free | Pro | Enterprise | | ------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------- | --------------------- | | Ad hoc generation on the homepage and `POST /v1/generate` | Unlimited, first 25 operations, one platform per run, nothing stored | Same | Same | | Hosted generations | Unlimited, first 25 endpoints each | Unlimited, whole spec | Unlimited, whole spec | | Platforms per project | 1: an SDK in one language | Any, billed each | Any, billed each | | Endpoints included per project | 25 | 250, then $1.50 per endpoint per month | Custom | | Regeneration pull requests, changelog, history, spec versions | Manual runs only; automatic regeneration is Pro | Included | Included | | Spec patches and config | Applied to every generation | Included | Included | | Preview builds on spec PRs | | Unlimited | Unlimited | | Hosted MCP endpoint | | | Included | | Webhook relay | | Included | Included | | Attribution in generated packages | On every plan | On every plan | On every plan | The endpoint meter is measured on the spec after patches, so removing operations with a patch reduces what is metered. It is counted once per project, whatever a project generates: endpoints belong to the spec, so a project building three languages does not pay for its endpoints three times. ## Hard limits | Limit | Value | | -------------------------------- | ------------------------------------------------------------------------------------- | | Spec size | 10MB, pasted or fetched | | Pasted spec in a browser request | About 4.5MB. Larger specs go in by URL. | | URL fetch timeout | 15 seconds | | Inline files in an API response | 3.5MB. Above that, `files_omitted` and a per-file endpoint. | | Spec patches per project | 50 | | Global parameters | 20 | | Pagination rules | 100 keys | | API list page size | 100 | | Anonymous generation | 20 requests per minute per IP | | Authenticated API requests | 60 requests per minute per key | | Webhook relay | 1MB per event, 5,000 events per session, events kept 24 hours, sessions 48 hours idle | | Project name | 80 characters | | Package name | 214 characters, valid for its registry | ## What is stored * **Ad hoc generation** stores nothing. Files are returned and forgotten. * **Projects** keep every generation's files, warnings, and metadata, plus every distinct spec version's raw text. Deleting a project deletes all of it. * **Preview builds** keep their generated files like any generation, and are not metered. * **Relay events** are transient: 24 hours at most. * **API keys** are stored as a hash. The full key exists only in the response that created it. --- # Go Source: https://typeship.dev/docs/sdks/go.md The Go client for typeship's own API. Generated by typeship from its own spec, so it is also a sample of what your Go users get. `github.com/typeship-ax/go` is the Go SDK for the typeship API, generated by typeship from [its own OpenAPI spec](https://typeship.dev/openapi.yaml). It has the same shape as every Go SDK typeship generates: `net/http` only, context-first methods, `(T, error)` returns, typed error structs, iterators for pagination, and gofmt-clean source. If you want to see what your Go users will get before you generate anything, read this module. ## Install ```bash go get github.com/typeship-ax/go ``` Go 1.21 or newer. `go.mod` has no `require` block. ## Create a client The module path ends in `go`, but the package it declares is `typeship`, which is the identifier the import binds and the one you type: ```go import "github.com/typeship-ax/go" client, err := typeship.New(typeship.WithBearerToken(os.Getenv("TYPESHIP_TOKEN"))) if err != nil { return err } ``` The key comes from the console under **api keys**. Pass it, or set `TYPESHIP_TOKEN`. `New` takes the same options as any generated Go client: `WithBaseURL`, `WithTimeout`, `WithMaxRetries`, `WithHTTPClient`, `WithOnRequest`, `WithOnResponse`, `WithOnError`, `WithDebug`, and `WithValidation`. See [Go](https://typeship.dev/docs/platforms/sdk/go). ## Generate a package `Generate.Run` runs the generator on a spec and returns the files. Nothing is stored. The free plan generates the first 25 operations; paid plans generate the whole spec: ```go result, err := client.Generate.Run(ctx, &typeship.GenerateRunParams{ Spec: typeship.SpecInput{URL: typeship.Ptr("https://api.acme.example.com/openapi.json")}, Platforms: []string{"sdk", "cli"}, Language: typeship.Ptr("go"), }) if err != nil { return err } for _, file := range result.Files { path := filepath.Join("out", file.Path) os.MkdirAll(filepath.Dir(path), 0o755) os.WriteFile(path, []byte(file.Content), 0o644) } fmt.Println(*result.Meta.OperationCount, "operations") ``` ## Work with projects ```go project, err := client.Projects.Create(ctx, &typeship.ProjectsCreateParams{ Name: "Acme API", SpecURL: typeship.Ptr("https://api.acme.example.com/openapi.json"), Platforms: []string{"sdk", "cli", "mcp"}, Languages: []string{"typescript", "go"}, Destinations: map[string]typeship.Destination{ "typescript": {Repo: typeship.Ptr("acme/acme-node")}, "go": {Repo: typeship.Ptr("acme/acme-go")}, }, AutoRegen: typeship.Ptr(true), }) if err != nil { return err } // Regenerate on demand. Each entry is a Generation or a GenerationFailure, // so the item is a union with an accessor per variant. generations, err := client.Projects.Generate(ctx, project.ID) if err != nil { return err } for _, item := range generations.Data { if generation, err := item.AsGeneration(); err == nil && generation.Status == "succeeded" { fmt.Println(*generation.Language, *generation.Meta.FileCount) } } // Walk history: the iterator fetches every page. it := client.Projects.ListGenerations(ctx, project.ID, nil) for it.Next() { generation := it.Value() fmt.Println(generation.CreatedAt, generation.Trigger, generation.Status) } if err := it.Err(); err != nil { return err } ``` ## Every method | Service | Methods | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Generate` | `Run(ctx, params)` | | `Projects` | `List(ctx, params)`, `Create(ctx, params)`, `Get(ctx, id)`, `Update(ctx, id, params)`, `Delete(ctx, id)`, `Generate(ctx, id)`, `ListGenerations(ctx, id, params)` | | `Generations` | `Get(ctx, id)`, `GetFile(ctx, id, params)` | | `SpecVersions` | `List(ctx, projectID, params)`, `Get(ctx, id)`, `GetContent(ctx, id)` | | `Account` | `Me(ctx)` | | `Usage` | `Retrieve(ctx)` | | `APIKeys` | `List(ctx, params)`, `Revoke(ctx, id)` | Every method takes variadic `RequestOption`s last. Struct fields carry the API's `snake_case` names in their JSON tags. `api.md` inside the module is the complete reference, and the [API reference](https://typeship.dev/docs/api) has every schema. ## Errors Every method returns `(T, error)`. Each documented status has its own type, and all of them unwrap to `*APIError`: ```go _, err := client.Projects.Generate(ctx, "prj_...") var payment *typeship.PaymentRequiredError var notFound *typeship.NotFoundError switch { case errors.As(err, &payment): // free plan allowance used up; payment.Message says so case errors.As(err, ¬Found): // no such project on this account } ``` See [Errors](https://typeship.dev/docs/typeship-api/api/errors) for the envelope and codes. --- # Python Source: https://typeship.dev/docs/sdks/python.md The Python client for typeship's own API. Generated by typeship from its own spec, so it is also a sample of what your Python users get. `typeship` on PyPI is the Python SDK for the typeship API, generated by typeship from [its own OpenAPI spec](https://typeship.dev/openapi.yaml). It has the same shape as every Python SDK typeship generates: zero runtime dependencies, `TypedDict` payloads, typed exceptions, auto-pagination, retries, and an async client. If you want to see what your Python users will get before you generate anything, read this package. ## Install ```bash pip install typeship ``` Python 3.11 or newer. Nothing else is installed. ## Create a client ```python import os from typeship import TypeshipClient client = TypeshipClient(bearer_token=os.environ["TYPESHIP_TOKEN"]) ``` The key comes from the console under **api keys**. Pass it, or set `TYPESHIP_TOKEN`. `TypeshipClient` takes the same options as any generated Python client: `base_url`, `timeout`, `max_retries`, `default_headers`, `transport`, `on_request`, `on_response`, `on_error`, `debug`, and `validate`. See [Python](https://typeship.dev/docs/platforms/sdk/python). ## Generate a package `generate.run` runs the generator on a spec and returns the files. Nothing is stored. The free plan generates the first 25 operations; paid plans generate the whole spec: ```python from pathlib import Path result = client.generate.run( spec={"url": "https://api.acme.example.com/openapi.json"}, platforms=["sdk", "cli"], language="python", ) for file in result["files"]: target = Path("out") / file["path"] target.parent.mkdir(parents=True, exist_ok=True) target.write_text(file["content"]) print(result["meta"]["operation_count"], "operations") ``` ## Work with projects ```python project = client.projects.create( name="Acme API", spec_url="https://api.acme.example.com/openapi.json", platforms=["sdk", "cli", "mcp"], languages=["typescript", "python"], destinations={ "typescript": {"repo": "acme/acme-node"}, "python": {"repo": "acme/acme-python"}, }, auto_regen=True, ) # Regenerate on demand and read every language's files. for generation in client.projects.generate(project["id"])["data"]: if generation["status"] != "succeeded": continue print(generation["language"], generation["meta"]["file_count"]) # Walk history: the iterator fetches every page. for generation in client.projects.list_generations(project["id"]): print(generation["created_at"], generation["trigger"], generation["status"]) ``` ## Async `AsyncTypeshipClient` has the same methods, awaitable: ```python import asyncio from typeship import AsyncTypeshipClient async def main() -> None: async with AsyncTypeshipClient(bearer_token=os.environ["TYPESHIP_TOKEN"]) as client: me = await client.account.me() async for project in client.projects.list(): print(project["name"]) asyncio.run(main()) ``` ## Every method | Resource | Methods | | --------------- | -------------------------------------------------------------------------------------------------------------------- | | `generate` | `run(*, spec, platforms=None, language=None, package_name=None, config=None)` | | `projects` | `list()`, `create(*, name, ...)`, `get(id)`, `update(id, ...)`, `delete(id)`, `generate(id)`, `list_generations(id)` | | `generations` | `get(id)`, `get_file(id, *, path)` | | `spec_versions` | `list(project_id)`, `get(id)`, `get_content(id)` | | `account` | `me()` | | `usage` | `retrieve()` | | `api_keys` | `list()`, `revoke(id)` | Paginated methods have a `_page` twin (`projects.list_page()`) that returns one envelope. Bodies and responses use the API's `snake_case` names. `api.md` inside the package is the complete reference, and the [API reference](https://typeship.dev/docs/api) has every schema. ## Errors Every call raises. Each documented status has its own class, and all of them are `ApiError`: ```python from typeship import NotFoundError, PaymentRequiredError try: client.projects.generate("prj_...") except PaymentRequiredError as exc: # free plan allowance used up; exc.body["errors"][0]["message"] says so ... except NotFoundError: # no such project on this account ... ``` See [Errors](https://typeship.dev/docs/typeship-api/api/errors) for the envelope and codes. --- # TypeScript Source: https://typeship.dev/docs/sdks/typescript.md The TypeScript client for typeship's own API. Generated by typeship from its own spec, so it is also a sample of what your users get. `typeship-ax` on npm is the TypeScript SDK for the typeship API, generated by typeship from [its own OpenAPI spec](https://typeship.dev/openapi.yaml). It has the same shape as every SDK typeship generates: zero runtime dependencies, typed results, typed error unions, auto-pagination, retries. If you want to see what your users will get before you generate anything, read this package. The package also carries the [typeship CLI](https://typeship.dev/docs/cli) and the [typeship MCP server](https://typeship.dev/docs/typeship-api/mcp). ## Install ```bash npm install typeship-ax ``` Node 18 or newer. ESM only. ## Create a client ```ts import { TypeshipClient } from "typeship-ax"; const client = new TypeshipClient({ bearerToken: process.env.TYPESHIP_TOKEN! }); ``` The key comes from the console under **api keys**. Pass it explicitly. The typeship SDK does not read environment variables for credentials. `TypeshipClient` takes the same options as any generated client: `baseUrl`, `timeoutMs`, `maxRetries`, `defaultHeaders`, `fetch`, `onRequest`, `onResponse`, `onError`, `debug`, and `validate`. See [Client options](https://typeship.dev/docs/platforms/sdk#client-options). ## Generate a package `generate.run` runs the generator on a spec and returns the files. Nothing is stored. The free plan generates the first 25 operations; paid plans generate the whole spec: ```ts const result = await client.generate.run({ spec: { url: "https://api.acme.example.com/openapi.json" }, platforms: ["sdk", "cli"], language: "typescript", }); if (result.ok) { for (const file of result.data.files) { await fs.writeFile(path.join("out", file.path), file.content); } console.log(result.data.meta.operation_count, "operations"); } ``` ## Work with projects ```ts import { TypeshipClient, unwrap } from "typeship-ax"; const client = new TypeshipClient({ bearerToken: process.env.TYPESHIP_TOKEN! }); const project = unwrap(await client.projects.create({ name: "Acme API", spec_url: "https://api.acme.example.com/openapi.json", platforms: ["sdk", "cli", "mcp"], languages: ["typescript", "python"], destinations: { typescript: { repo: "acme/acme-node" }, python: { repo: "acme/acme-python" }, }, auto_regen: true, })); // Regenerate on demand and read every language's files. const generations = unwrap(await client.projects.generate(project.id)); for (const generation of generations.data) { if (generation.status !== "succeeded") continue; console.log(generation.language, generation.meta.file_count); } // Walk history. for await (const generation of client.projects.listGenerations(project.id)) { console.log(generation.created_at, generation.trigger, generation.status); } ``` ## Every method | Resource | Methods | | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `generate` | `run(body)` | | `projects` | `list(params?)`, `create(body)`, `get(id)`, `update(id, body)`, `delete(id)`, `generate(id)`, `listGenerations(id, params?)` | | `generations` | `get(id)`, `getFile(id, { path })` | | `specVersions` | `list(projectId, params?)`, `get(id)`, `getContent(id)` | | `account` | `me()` | | `usage` | `retrieve()` | | `apiKeys` | `list(params?)`, `revoke(id)` | Bodies and responses use the API's `snake_case` names. `api.md` inside the package is the complete reference, and the [API reference](https://typeship.dev/docs/api) has every schema. ## Errors Every call returns `ApiResult`. The error union for each operation lists exactly what the API documents for it, plus `UnexpectedApiError`, `TransportError`, and `ValidationError`: ```ts import { NotFoundError, PaymentRequiredError } from "typeship-ax"; const result = await client.projects.generate("prj_..."); if (!result.ok) { if (result.error instanceof PaymentRequiredError) { // free plan allowance used up; result.error.body.errors[0].message says so } if (result.error instanceof NotFoundError) { // no such project on this account } } ``` See [Errors](https://typeship.dev/docs/typeship-api/api/errors) for the envelope and codes. --- # Reference: typeship HTTP API # typeship HTTP API Base URL: `https://typeship.dev/api/v1`. Every operation takes an API key sent as `Authorization: Bearer ak_...`. Conventions: snake_case JSON, ISO 8601 timestamps, cursor pagination (`limit` + `cursor` -> `data` / `next_cursor`), errors as `{ "errors": [{ "code", "message" }], "request_id" }` with the id mirrored on the `x-request-id` header. The full OpenAPI document is at /openapi.yaml. ## generate ### POST /generate API key required. Generate a package from a spec **Body (JSON)** | name | type | description | | --- | --- | --- | | spec | `SpecInput` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to generate from the spec. Defaults to [sdk]. | | language? | `"typescript" | "python" | "go"` | Language to generate. Python and Go produce the SDK only; the CLI and MCP server are TypeScript artifacts and are skipped with a warning when requested alongside them. | | package_name? | `string` | npm name override for the generated package. | | config? | `Config` | | ```sh curl -s https://typeship.dev/api/v1/generate \ -H "Authorization: Bearer ak_..." \ -H "Content-Type: application/json" \ -d '{"spec":"…"}' ``` Responses: 200 (The generated package.); 401 (Missing or invalid API key.); 413 (Spec exceeds the 10MB limit.); 422 (The spec could not be understood.); default (Unexpected error.). ## projects ### GET /projects API key required. Paginated. List projects **Query** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | ```sh curl -s https://typeship.dev/api/v1/projects \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (A page of projects.); 401 (Missing or invalid API key.). ### POST /projects API key required. Create a project **Body (JSON)** | name | type | description | | --- | --- | --- | | name | `string` | | | spec_url? | `string` | Spec location for a URL-sourced project. Provide this or source; a project with neither has nothing to generate. | | source? | `Source` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to build. sdk is implied; cli and mcp require typescript among the languages. Free projects run one platform in total (one SDK language); more is a 402 until the account is on Pro. | | languages? | `Array<"typescript" | "python" | "go">` | Languages to generate. Each is a separate package, a separate pull request, a separate hosted generation, and one platform for billing. Defaults to typescript alone. | | destinations? | `Record` | Per-language pull-request destination, keyed by language. | | package_names? | `Record` | Registry name per language; unset derives from the API title. | | destination? | `Destination | null` | | | auto_regen? | `boolean` | | | package_name? | `string | null` | | | spec_patches? | `SpecPatch[]` | | | mcp_enabled? | `boolean` | Requires the mcp platform and Enterprise. | | relay_enabled? | `boolean` | Requires the cli platform and Pro. | | config? | `Config | null` | | ```sh curl -s https://typeship.dev/api/v1/projects \ -H "Authorization: Bearer ak_..." \ -H "Content-Type: application/json" \ -d '{"name":"…"}' ``` Responses: 201 (Created.); 400 (Invalid name, spec source, or field value.); 401 (Missing or invalid API key.); 402 (The plan does not include this.). ### GET /projects/{project_id} API key required. Retrieve a project ```sh curl -s https://typeship.dev/api/v1/projects/ \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The project.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### DELETE /projects/{project_id} API key required. Delete a project ```sh curl -s -X DELETE https://typeship.dev/api/v1/projects/ \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (Deleted.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### PATCH /projects/{project_id} API key required. Update a project **Body (JSON)** | name | type | description | | --- | --- | --- | | name? | `string` | | | spec_url? | `string` | | | source? | `Source` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to build; replaces the list. Dropping cli or mcp turns off the hosted feature it serves. cli and mcp require typescript among the languages. Turning a platform off stops generating it; nothing already delivered is removed. | | destination? | `Destination | null` | | | languages? | `Array<"typescript" | "python" | "go">` | Languages to generate; replaces the list. Each is its own hosted generation and one platform for billing. | | destinations? | `Record` | Per-language pull-request destination, keyed by language. | | package_names? | `Record` | Registry name per language; unset derives from the API title. | | auto_regen? | `boolean` | | | package_name? | `string | null` | | | spec_patches? | `SpecPatch[]` | | | mcp_enabled? | `boolean` | Serve this project as a hosted remote MCP endpoint. Requires the mcp platform and Enterprise. | | relay_enabled? | `boolean` | Enable the webhook relay so the generated CLI's webhooks listen command works for this API's users. Requires the cli platform and Pro. | | config? | `Config | null` | Replaces the whole config. Pass null to clear it. | ```sh curl -s -X PATCH https://typeship.dev/api/v1/projects/ \ -H "Authorization: Bearer ak_..." \ -H "Content-Type: application/json" \ -d '{"name":"…","spec_url":"https://api.example.com/openapi.json"}' ``` Responses: 200 (The updated project.); 400 (Invalid field.); 401 (Missing or invalid API key.); 402 (The plan does not include this.); 404 (No such resource in this account.). ### GET /projects/{project_id}/generations API key required. Paginated. List a project's generations **Query** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | | language? | `"typescript" | "python" | "go"` | Only generations for this language. | ```sh curl -s https://typeship.dev/api/v1/projects//generations \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (A page of generations, newest first, without files.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### POST /projects/{project_id}/generations API key required. Run a hosted generation ```sh curl -s -X POST https://typeship.dev/api/v1/projects//generations \ -H "Authorization: Bearer ak_..." ``` Responses: 201 (One entry per configured language: the stored generation including files, or a failure record for a language that did not generate.); 401 (Missing or invalid API key.); 402 (The account has used its hosted generation allowance.); 404 (No such resource in this account.); 422 (The spec could not be understood.). ### GET /projects/{project_id}/mcp_usage API key required. Retrieve hosted MCP endpoint usage for a project **Query** | name | type | description | | --- | --- | --- | | days? | `number` | Window in days, 1 to 90. | ```sh curl -s https://typeship.dev/api/v1/projects//mcp_usage \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (Usage over the window.); 400 (days out of range.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ## generations ### GET /generations/{generation_id} API key required. Retrieve a generation ```sh curl -s https://typeship.dev/api/v1/generations/ \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The generation.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### GET /generations/{generation_id}/file API key required. Fetch one file from a generation **Query** | name | type | description | | --- | --- | --- | | path | `string` | Repo-relative path inside the generated package. | ```sh curl -s "https://typeship.dev/api/v1/generations//file?path=" \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The file content.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ## specVersions ### GET /projects/{project_id}/spec_versions API key required. Paginated. List the specs this project has generated from **Query** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | ```sh curl -s https://typeship.dev/api/v1/projects//spec_versions \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (A page of spec versions.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### GET /spec_versions/{spec_version_id} API key required. Retrieve a spec version ```sh curl -s https://typeship.dev/api/v1/spec_versions/ \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The spec version.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ### GET /spec_versions/{spec_version_id}/content API key required. Retrieve a spec version's raw text ```sh curl -s https://typeship.dev/api/v1/spec_versions//content \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The raw spec text.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). ## account ### GET /me API key required. The account behind the presented credentials ```sh curl -s https://typeship.dev/api/v1/me \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The authenticated account.); 401 (Missing or invalid API key.). ## usage ### GET /usage API key required. Retrieve usage for this account ```sh curl -s https://typeship.dev/api/v1/usage \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (Current usage.); 401 (Missing or invalid API key.). ## apiKeys ### GET /api_keys API key required. Paginated. List API keys **Query** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | ```sh curl -s https://typeship.dev/api/v1/api_keys \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (A page of API keys.); 401 (Missing or invalid API key.). ### DELETE /api_keys/{api_key_id} API key required. Revoke an API key ```sh curl -s -X DELETE https://typeship.dev/api/v1/api_keys/ \ -H "Authorization: Bearer ak_..." ``` Responses: 200 (The revoked key.); 401 (Missing or invalid API key.); 404 (No such resource in this account.). --- # Reference: typeship SDK # typeship — SDK reference Generated from typeship's OpenAPI spec via the same IR that generates the SDK. Client: `TypeshipClient`. Base URL: `https://typeship.dev/api/v1`. ```ts import { TypeshipClient } from "typeship"; const client = new TypeshipClient({ bearerToken: process.env.TYPESHIP_API_KEY, // ak_..., optional for generate }); ``` ## generate ### generate.run `POST /generate` Generate a package from a spec ```ts client.generate.run(body, options?) ``` **Body** | name | type | description | | --- | --- | --- | | spec | `SpecInput` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to generate from the spec. Defaults to [sdk]. | | language? | `"typescript" | "python" | "go"` | Language to generate. Python and Go produce the SDK only; the CLI and MCP server are TypeScript artifacts and are skipped with a warning when requested alongside them. | | package_name? | `string` | npm name override for the generated package. | | config? | `Config` | | Returns `Promise>`. Errors: UnauthorizedError, PayloadTooLargeError, UnprocessableEntityError, ApiResponseError, UnexpectedApiError, TransportError. ```ts const result = await client.generate.run({ spec: "…", }); if (result.ok) { result.data; // GenerationResult } else { result.error; // GenerateRunError } ``` ## projects ### projects.list `GET /projects` (paginated) List projects ```ts client.projects.list(params?, options?) ``` **Params** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | Returns `PagePromise`. Errors: UnauthorizedError, UnexpectedApiError, TransportError. ```ts // one page: const page = await client.projects.list(); if (page.ok) { page.data.items; // Project[] page.data.hasNextPage(); } // or every item across every page: for await (const item of client.projects.list()) { console.log(item); } ``` ### projects.create `POST /projects` Create a project ```ts client.projects.create(body, options?) ``` **Body** | name | type | description | | --- | --- | --- | | name | `string` | | | spec_url? | `string` | Spec location for a URL-sourced project. Provide this or source; a project with neither has nothing to generate. | | source? | `Source` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to build. sdk is implied; cli and mcp require typescript among the languages. Free projects run one platform in total (one SDK language); more is a 402 until the account is on Pro. | | languages? | `Array<"typescript" | "python" | "go">` | Languages to generate. Each is a separate package, a separate pull request, a separate hosted generation, and one platform for billing. Defaults to typescript alone. | | destinations? | `Record` | Per-language pull-request destination, keyed by language. | | package_names? | `Record` | Registry name per language; unset derives from the API title. | | destination? | `Destination | null` | | | auto_regen? | `boolean` | | | package_name? | `string | null` | | | spec_patches? | `SpecPatch[]` | | | mcp_enabled? | `boolean` | Requires the mcp platform and Enterprise. | | relay_enabled? | `boolean` | Requires the cli platform and Pro. | | config? | `Config | null` | | Returns `Promise>`. Errors: BadRequestError, UnauthorizedError, PaymentRequiredError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.create({ name: "…", }); if (result.ok) { result.data; // Project } else { result.error; // ProjectsCreateError } ``` ### projects.get `GET /projects/{project_id}` Retrieve a project ```ts client.projects.get(projectId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.get(""); if (result.ok) { result.data; // Project } else { result.error; // ProjectsGetError } ``` ### projects.delete `DELETE /projects/{project_id}` Delete a project ```ts client.projects.delete(projectId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.delete(""); if (result.ok) { result.data; // { deleted: true; } } else { result.error; // ProjectsDeleteError } ``` ### projects.update `PATCH /projects/{project_id}` Update a project ```ts client.projects.update(projectId: string, body, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | **Body** | name | type | description | | --- | --- | --- | | name? | `string` | | | spec_url? | `string` | | | source? | `Source` | | | platforms? | `Array<"sdk" | "cli" | "mcp">` | Artifacts to build; replaces the list. Dropping cli or mcp turns off the hosted feature it serves. cli and mcp require typescript among the languages. Turning a platform off stops generating it; nothing already delivered is removed. | | destination? | `Destination | null` | | | languages? | `Array<"typescript" | "python" | "go">` | Languages to generate; replaces the list. Each is its own hosted generation and one platform for billing. | | destinations? | `Record` | Per-language pull-request destination, keyed by language. | | package_names? | `Record` | Registry name per language; unset derives from the API title. | | auto_regen? | `boolean` | | | package_name? | `string | null` | | | spec_patches? | `SpecPatch[]` | | | mcp_enabled? | `boolean` | Serve this project as a hosted remote MCP endpoint. Requires the mcp platform and Enterprise. | | relay_enabled? | `boolean` | Enable the webhook relay so the generated CLI's webhooks listen command works for this API's users. Requires the cli platform and Pro. | | config? | `Config | null` | Replaces the whole config. Pass null to clear it. | Returns `Promise>`. Errors: BadRequestError, UnauthorizedError, PaymentRequiredError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.update("", { name: "…", spec_url: "https://api.example.com/openapi.json", }); if (result.ok) { result.data; // Project } else { result.error; // ProjectsUpdateError } ``` ### projects.listGenerations `GET /projects/{project_id}/generations` (paginated) List a project's generations ```ts client.projects.listGenerations(projectId: string, params?, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | **Params** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | | language? | `"typescript" | "python" | "go"` | Only generations for this language. | Returns `PagePromise`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts // one page: const page = await client.projects.listGenerations(""); if (page.ok) { page.data.items; // Generation[] page.data.hasNextPage(); } // or every item across every page: for await (const item of client.projects.listGenerations("")) { console.log(item); } ``` ### projects.generate `POST /projects/{project_id}/generations` Run a hosted generation ```ts client.projects.generate(projectId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, PaymentRequiredError, NotFoundError, UnprocessableEntityError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.generate(""); if (result.ok) { result.data; // { data: Array; } } else { result.error; // ProjectsGenerateError } ``` ### projects.mcpUsage `GET /projects/{project_id}/mcp_usage` Retrieve hosted MCP endpoint usage for a project ```ts client.projects.mcpUsage(projectId: string, params?, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | **Params** | name | type | description | | --- | --- | --- | | days? | `number` | Window in days, 1 to 90. | Returns `Promise>`. Errors: BadRequestError, UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.projects.mcpUsage(""); if (result.ok) { result.data; // McpUsage } else { result.error; // ProjectsMcpUsageError } ``` ## generations ### generations.get `GET /generations/{generation_id}` Retrieve a generation ```ts client.generations.get(generationId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | generation_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.generations.get(""); if (result.ok) { result.data; // Generation } else { result.error; // GenerationsGetError } ``` ### generations.getFile `GET /generations/{generation_id}/file` Fetch one file from a generation ```ts client.generations.getFile(generationId: string, params, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | generation_id | `string` | | **Params** | name | type | description | | --- | --- | --- | | path | `string` | Repo-relative path inside the generated package. | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.generations.getFile(""); if (result.ok) { result.data; // string } else { result.error; // GenerationsGetFileError } ``` ## specVersions ### specVersions.list `GET /projects/{project_id}/spec_versions` (paginated) List the specs this project has generated from ```ts client.specVersions.list(projectId: string, params?, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | project_id | `string` | | **Params** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | Returns `PagePromise`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts // one page: const page = await client.specVersions.list(""); if (page.ok) { page.data.items; // SpecVersion[] page.data.hasNextPage(); } // or every item across every page: for await (const item of client.specVersions.list("")) { console.log(item); } ``` ### specVersions.get `GET /spec_versions/{spec_version_id}` Retrieve a spec version ```ts client.specVersions.get(specVersionId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | spec_version_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.specVersions.get(""); if (result.ok) { result.data; // SpecVersion } else { result.error; // SpecVersionsGetError } ``` ### specVersions.getContent `GET /spec_versions/{spec_version_id}/content` Retrieve a spec version's raw text ```ts client.specVersions.getContent(specVersionId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | spec_version_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.specVersions.getContent(""); if (result.ok) { result.data; // string } else { result.error; // SpecVersionsGetContentError } ``` ## account ### account.me `GET /me` The account behind the presented credentials ```ts client.account.me(options?) ``` Returns `Promise>`. Errors: UnauthorizedError, UnexpectedApiError, TransportError. ```ts const result = await client.account.me(); if (result.ok) { result.data; // Account } else { result.error; // AccountMeError } ``` ## usage ### usage.retrieve `GET /usage` Retrieve usage for this account ```ts client.usage.retrieve(options?) ``` Returns `Promise>`. Errors: UnauthorizedError, UnexpectedApiError, TransportError. ```ts const result = await client.usage.retrieve(); if (result.ok) { result.data; // Usage } else { result.error; // UsageRetrieveError } ``` ## apiKeys ### apiKeys.list `GET /api_keys` (paginated) List API keys ```ts client.apiKeys.list(params?, options?) ``` **Params** | name | type | description | | --- | --- | --- | | limit? | `number` | | | cursor? | `string` | | Returns `PagePromise`. Errors: UnauthorizedError, UnexpectedApiError, TransportError. ```ts // one page: const page = await client.apiKeys.list(); if (page.ok) { page.data.items; // ApiKey[] page.data.hasNextPage(); } // or every item across every page: for await (const item of client.apiKeys.list()) { console.log(item); } ``` ### apiKeys.revoke `DELETE /api_keys/{api_key_id}` Revoke an API key ```ts client.apiKeys.revoke(apiKeyId: string, options?) ``` **Path arguments** | name | type | description | | --- | --- | --- | | api_key_id | `string` | | Returns `Promise>`. Errors: UnauthorizedError, NotFoundError, UnexpectedApiError, TransportError. ```ts const result = await client.apiKeys.revoke(""); if (result.ok) { result.data; // ApiKey } else { result.error; // ApiKeysRevokeError } ``` ## Types ```ts // typeship — API types. // Generated by typeship — https://typeship.dev — do not edit by hand. /** The spec to generate from. Provide exactly one of url or inline. */ export interface SpecInput { /** * Publicly reachable URL of an OpenAPI document, a GraphQL SDL * file, or a GraphQL endpoint (introspected automatically). * Fetched server-side. */ url?: string; /** Raw spec text (OpenAPI JSON/YAML or GraphQL SDL). Up to 10MB. */ inline?: string; } export interface GeneratedFile { /** Repo-relative path inside the generated package. */ path: string; content: string; } export interface GenerationMeta { title: string; version: string; spec_format?: "openapi" | "graphql"; /** Detected spec version, "2.0", "3.0", or "3.1". */ oas_version: string; /** True when the input was Swagger 2.0 and was converted. */ converted?: boolean; package_name: string; client_name: string; targets: Array<"sdk" | "cli" | "mcp">; resource_count?: number; operation_count?: number; schema_count?: number; paginated_operation_count?: number; /** Operations beyond the plan's endpoint allowance, not generated. */ omitted_operation_count?: number; /** Pull request opened by this regeneration, when one was. */ pr_url?: string | null; pr_number?: number | null; /** Markdown changelog entry for this regeneration, from the API surface diff. Absent on a first generation or when nothing changed. */ changelog?: string; /** Breaking changes in the diff; removed methods and fields, changed types, inputs that became required. */ breaking_count?: number; /** What the diff was measured against; "destination" means the .typeship/surface.json merged in the destination repository. */ baseline?: "destination" | "last-generation" | "none"; /** The typeship/semver verdict on the regeneration pull request; failure means breaking changes without a major version bump. */ semver?: "success" | "failure"; /** The verdict in one line, as the commit status describes it. */ semver_note?: string; /** The package version the destination had before this regeneration. */ previous_version?: string; file_count?: number; total_lines?: number; } export interface GenerationResult { files: GeneratedFile[]; warnings: string[]; meta: GenerationMeta; limits?: GenerationLimits; /** Anonymous, URL-sourced generations only. A link a signed-in person can open to turn this run into a project in their organization (same spec, language, platforms, config). Lasts seven days. Null for inline specs; absent on keyed calls. */ claim?: null | { url: string; /** Format: date-time */ expires_at: string; }; } /** Present when the generation was capped: by the free plan, or because the call was anonymous. Absent on uncapped generations. */ export interface GenerationLimits { /** How many operations this generation was allowed to include. */ max_operations: number; /** How many operations in the spec were left out. */ omitted_operations: number; reason: "anonymous" | "free_plan"; /** Anonymous calls only. Where to create an account. */ signup_url?: string; /** Where the cap is lifted. */ upgrade_url: string; } /** Where the project's spec lives. */ export interface Source { kind: "url" | "repo"; /** kind url. Fetched server-side for every generation. */ url?: string | null; /** kind repo, "owner/name". Watched via the GitHub App. */ repo?: string | null; /** Path of the spec file inside the repository. */ path?: string | null; } /** * A fix applied to the spec before generation. Targets are JSON * Pointers into the document. A patch whose target no longer exists is * skipped and reported as a warning on the generation, never silently. */ export interface SpecPatch { op: "set" | "append" | "remove" | "rename"; /** * JSON-Pointer-style path. Pattern segments enable bulk fixes: * * (any child), ** (any depth), [key=value] (filter), e.g. * /paths/**\/parameters/[name=account_id]/schema/type. Renaming a * schema under /components/schemas also rewrites its $refs. */ path: string; /** set only; the replacement value. */ value?: unknown; /** rename only; the new key name. */ to?: string | null; reason?: string | null; } /** Where regeneration pull requests land. */ export interface Destination { /** Defaults to the source repository when the source is a repo. */ repo?: string | null; /** Directory the generated package is written to. */ directory?: string | null; } export interface Project { id: string; object: "project"; name: string; /** The source URL when the source kind is url; null otherwise. */ spec_url?: string | null; source: Source; destination?: Destination | null; /** Languages this project generates. Each is a separate package, a separate pull request, and a separate hosted generation. Defaults to typescript alone. */ languages?: Array<"typescript" | "python" | "go">; /** Where each language's pull request lands, keyed by language. A repository each is the convention API vendors follow, and Go requires it since `go get` resolves a module to the repository root. Several languages may share a repository with different directories, producing one pull request. */ destinations?: Record; /** Registry name per language. The ecosystems disagree about what a name is: npm takes an optional @scope, PyPI normalizes to lowercase-with-hyphens, and Go's name is the module path that `go get` resolves. Unset means the name is derived from the API's title. */ package_names?: Record; /** Regenerate when the spec changes: on every push to the default branch for a repository source, every 30 minutes for a URL source. Off by default: the first generation is always one you asked for. Off means only "generate now" and POST /projects/{project_id}/generations regenerate. */ auto_regen: boolean; /** npm name override for generated output; supports @scope/name. */ package_name?: string | null; spec_patches?: SpecPatch[]; config?: Config | null; /** Whether the hosted MCP endpoint is on. Requires the mcp platform and Enterprise; turning the platform off turns this off. */ mcp_enabled?: boolean; /** Path of the hosted MCP endpoint while it is on; read-only. */ mcp_url?: string | null; /** Whether the webhook relay is on, letting the generated CLI's webhooks listen command mint relay sessions. Requires the cli platform and Pro; turning the platform off turns this off. */ relay_enabled?: boolean; /** Artifacts this project builds from its spec. sdk is always present and stands for the SDK in each of `languages`; cli and mcp are built on the TypeScript SDK and ship in its package, so they require typescript among the languages. Each SDK language and each of cli and mcp is one platform for billing. */ platforms: Array<"sdk" | "cli" | "mcp">; /** Format: date-time */ created_at: string; } /** The organization an API key belongs to. Members share its projects, keys, and plan; sign-in identity is not part of the API. */ export interface Account { id: string; object: "account"; /** The organization's display name. */ name: string; plan: "free" | "pro" | "enterprise"; /** Format: date-time */ created_at: string; } /** How the generated CLI behaves. Part of Config. */ export interface CliBehavior { /** resource.method of a zero-argument GET that the generated CLI's whoami command calls. Overrides auto-detection; a value that matches nothing is reported as a generation warning. */ whoami_operation?: string | null; /** OAuth client id baked into the generated CLI for device-flow login. Without it, login prompts for a pasted credential. */ oauth_client_id?: string | null; /** Scopes requested during device-flow login. Include offline_access if the authorization server gates refresh tokens behind it. */ oauth_scopes?: string[]; /** Audience sent with the device-authorization request, for authorization servers that require one to issue API-valid access tokens. */ oauth_audience?: string | null; /** Opt in to a once-a-day registry check that prints an upgrade hint. Off by default; generated code phones nobody unless this is enabled. */ update_notice?: boolean; /** Where the generated CLI's feedback command sends users. GitHub issues/new URLs get a prefilled title and environment details. */ support_url?: string | null; } /** How the generated MCP server and the hosted endpoint behave. Part of Config. */ export interface McpBehavior { /** MCP tool shape. meta collapses per-operation tools into search_docs, read_docs, and execute so large APIs don't flood an agent's context window; auto switches to meta above 100 operations. */ tool_mode?: "auto" | "operations" | "meta"; /** Guidance appended to the MCP server's instructions, which agents read once when they connect (server/discover): what to call first, conventions the spec does not state, what not to do. Carried by the package's server and the hosted endpoint alike. */ instructions?: string | null; /** Hand-written MCP tool descriptions keyed by operationId or "METHOD /path". Each replaces the text typeship derives for that operation (summary, first sentence, method and path, deprecation and auth notes). For flows the spec cannot describe, such as a multi-step upload. Keys that match no operation are reported as generation warnings. */ tool_descriptions?: Record; } /** Everything typeship needs beyond the spec, in one object: generation customization (globals, retries, pagination) and how the generated tooling behaves (cli, mcp, docs_url). Plain configuration. typeship never requires vendor extensions inside the spec itself. The same shape is accepted on a project and on POST /generate. */ export interface Config { /** Wire names of query/header parameters that become settable once on the generated client and auto-apply to every operation that accepts them; per-call values win. Names that match nothing are reported as generation warnings. */ globals?: string[]; retries?: RetryTuning; /** Per-operation pagination control, keyed by operationId or "METHOD /path". Unmatched keys are reported as generation warnings. */ pagination?: Record; graphql?: GraphqlSettings; cli?: CliBehavior; mcp?: McpBehavior; /** The API's documentation site. Read through its llms.txt by the generated CLI's docs command, the MCP server's docs tools, and the package's AGENTS.md. Defaults to the spec's externalDocs URL. */ docs_url?: string | null; } /** What a GraphQL schema cannot say about itself. Ignored for OpenAPI specs. */ export interface GraphqlSettings { /** * The URL every request is POSTed to; the generated client's default baseUrl. Defaults to the URL the schema was fetched from. Without either, baseUrl is a required client option. * Format: uri */ endpoint?: string; /** Named endpoints (sandbox, production). Each becomes a client environment; the first is the default unless endpoint is set. */ environments?: Array<{ name: string; /** Format: uri */ url: string; }>; /** * How requests authenticate. bearer sends Authorization: Bearer; basic is for key-pair APIs (public key as username, private key as password); api_key sends a header named by api_key_header; none generates no auth option. * Default: "bearer" */ auth?: "bearer" | "basic" | "api_key" | "none"; /** Header carrying the key when auth is api_key. Default X-API-Key. */ api_key_header?: string; /** The API's name; drives the package and client names ("Braintree" gives braintree and BraintreeClient). Defaults to a name derived from the endpoint's host. */ title?: string; } /** Retry behavior. Top-level fields adjust every operation; operations maps operationId or "METHOD /path" keys to per-operation overrides. */ export interface RetryTuning { max_retries?: number; /** Replaces the default retryable set (408, 429, 500, 502, 503, 504). */ statuses?: number[]; initial_delay_ms?: number; max_delay_ms?: number; /** Also retry non-idempotent methods (POST/PATCH). */ retry_non_idempotent?: boolean; /** Shorthand for max_retries 0. */ disabled?: boolean; operations?: Record; } export interface PaginationRule { /** Default: "cursor" */ style?: "cursor" | "cursorFromLastId" | "page" | "offset"; /** Response field holding the item array. */ items_field: string; cursor_param?: string; next_cursor_field?: string; has_more_field?: string; id_field?: string; page_param?: string; offset_param?: string; limit_param?: string; } export interface FileStub { path: string; bytes: number; } export interface Generation { id: string; object: "generation"; /** Present and true when the generated output was too large to inline; files_index lists paths, fetched one at a time via GET /generations/{generation_id}/file. */ files_omitted?: boolean; files_index?: FileStub[]; project_id?: string | null; status: "succeeded" | "failed"; trigger: "manual" | "webhook" | "poll" | "preview"; /** Language this run generated. Null on generations recorded before projects had a language axis. */ language?: "typescript" | "python" | "go" | null; meta?: GenerationMeta; warnings?: string[]; /** Present on retrieve and create; omitted in lists. */ files?: GeneratedFile[]; error?: string | null; /** Format: date-time */ created_at: string; } /** A language that did not generate in a multi-language run. */ export interface GenerationFailure { language: "typescript" | "python" | "go"; status: "failed"; error: string; } export interface Usage { object: "usage"; hosted_generations: { used: number; /** Null on paid plans, which meter rather than cap. */ included?: number | null; remaining?: number | null; }; /** Endpoints included before per-endpoint billing applies. */ included_endpoints: number; /** Who called the API in the last 30 days, read from the User-Agent the generated tooling sends: by surface (cli, mcp, sdk, http) and by agent harness (claude-code, codex, cursor, ...), and the share of requests that came through an agent. */ requests?: { days: number; requests: number; by_surface: Record; by_harness: Record; /** 0 to 1. */ agent_share: number; }; } export interface ApiKey { id: string; object: "api_key"; name: string; /** Last four characters of the secret; the secret itself is never stored. */ last4: string; revoked: boolean; /** Format: date-time */ last_used_at?: string | null; /** Format: date-time */ created_at: string; } export interface McpUsage { object: "mcp_usage"; project_id: string; /** The hosted endpoint URL, or null when it is off. */ mcp_url?: string | null; /** The window these numbers cover. */ days: number; /** Tool calls served */ calls: number; /** Calls whose result was a tool error (API failures */ errors: number; /** Calls turned away by the per-caller or per-endpoint limit. */ rate_limited: number; /** Mean upstream request time across served calls. */ avg_duration_ms: number; by_tool: Array<{ tool: string; calls: number; errors: number; }>; } export interface SpecVersion { id: string; object: "spec_version"; project_id: string; /** sha256 of the raw spec text; the version's identity. */ hash: string; bytes?: number; /** Where this spec came from — a URL, or a repo and path. */ source?: Record | null; /** The raw spec text. Present on retrieve, omitted from lists, and replaced by content_omitted when the spec is too large to inline. */ content?: string; /** Present and true when the spec was too large to inline; fetch it from /spec_versions/{spec_version_id}/content. */ content_omitted?: boolean; /** Format: date-time */ created_at: string; } export interface ErrorModel { errors: Array<{ code: "invalid_request" | "unauthorized" | "not_found" | "spec_error" | "fetch_error" | "plan_limit_reached" | "payload_too_large" | "rate_limited" | "internal_error"; message: string; }>; /** Also sent as the x-request-id response header. */ request_id: string; } ```