How to Generate Anthropic Claude SDKs from OpenAPI Spec with Stainless in 2025
Prerequisites and What You'll Need
Before you generate your first SDK, get your environment squared away. Missing a tool or running the wrong version will cause cryptic errors mid-process, and the Stainless CLI is strict about its requirements.
- [ ] Node.js 20+ installed (
node --version) - [ ] Python 3.11+ with pip available (
python --version) - [ ] An Anthropic API key from console.anthropic.com
- [ ] A Stainless account (sign up at stainless.com) and workspace API token
- [ ] A valid OpenAPI 3.1 specification file for your API (or use the example in Step 2)
- [ ] Git initialized in your project directory (Stainless commits generated code by branch)
| Tool | Min Version | Install Command | Purpose |
|---|---|---|---|
| Node.js | 20.x | nvm install 20 | Runs the Stainless CLI and TypeScript SDK |
| npm | 10.x | Bundled with Node 20 | Installs @stainless-api/cli globally |
| Python | 3.11 | pyenv install 3.11 | Runs the generated Python SDK |
| Stainless CLI | latest | npm install -g @stainless-api/cli | Generates SDKs and MCP servers |
| OpenAPI spec | 3.1.0 | — | Source of truth for generated SDKs |
Note: Stainless has powered every official Anthropic SDK since the earliest days of the Claude API — including
anthropic-sdk-typescriptandanthropic-sdk-python. When Anthropic acquired Stainless in 2026, the SDK generation pipeline was already battle-tested across hundreds of companies. You're using the same toolchain that ships Claude's official libraries.
Estimated time: 35 minutes
Understanding How Stainless Powers the Claude SDK Ecosystem
Before you run a single command, it's worth understanding the model so you don't treat Stainless as a glorified code template engine — it's significantly more opinionated than that, and that's a feature.
How Stainless Turns an API Spec into Language-Native SDKs
Stainless takes an OpenAPI 3.1 specification as input and produces idiomatic, production-grade SDKs in TypeScript, Python, Go, Java, Kotlin, Ruby, and more. "Idiomatic" here is deliberate: the generator doesn't just serialize your endpoints into function calls. It analyzes resource hierarchies from your URL paths, infers pagination patterns from response shapes, wraps streaming endpoints with proper async iterators, and surfaces typed error classes that mirror your API's error schema.
The result is that a TypeScript developer gets async/await with fully typed MessageCreateParams, while a Python developer gets asyncio-compatible coroutines backed by Pydantic models — neither looks like auto-generated boilerplate.
What 'Feels Native' Means for TypeScript, Python, Go, and Java SDKs
"Feels native" is the hardest part of SDK design to get right manually, and where Stainless earns its value. For TypeScript, that means tree-shakeable ESM exports, discriminated union types for response variants, and auto-typed request builders. For Python, it means dataclasses or Pydantic models (your choice), context manager support for streaming, and httpx-based async transport. For Go, it means zero-allocation option patterns and struct embedding for request params. You don't have to configure most of this — Stainless infers it from your spec and applies per-language conventions automatically.
Why Anthropic Acquired Stainless and What That Means for Developers
Founder Alex Rattray described it plainly: Anthropic was one of the first teams to bet on Stainless, and the acquisition was about accelerating both SDK quality and MCP (Model Context Protocol) server generation. MCP is Anthropic's open protocol for giving agents structured access to external tools and data sources. Stainless now generates MCP servers directly from OpenAPI specs — meaning the same spec that generates your REST SDK can also produce a plug-in that lets a Claude agent call your API autonomously. For developers building agent-connected systems, this is a significant workflow compression.
Step 1 — Install and Configure the Stainless CLI
The CLI is the entry point for everything: initialization, validation, and generation. Install it globally so it's available in any project directory.
Installing Stainless via npm Globally
npm install -g @stainless-api/cli
# Verify installation
stainless --version
# → @stainless-api/cli 1.x.x
Authenticating with Your Stainless Workspace Credentials
Log in with your Stainless workspace token. You can find this under Settings → API Tokens in the Stainless dashboard.
stainless auth login --token stainless_tok_xxxxxxxxxxxxxxxxxxxx
# → Authenticated as your-org/your-workspace
# → Token saved to ~/.config/stainless/credentials.json
Initializing a New SDK Project with stainless init
mkdir my-claude-sdk && cd my-claude-sdk
git init
stainless init my-claude-sdk
# → Creating project: my-claude-sdk
# → Select languages to generate: [x] TypeScript [x] Python [ ] Go [ ] Java
# → OpenAPI spec path or URL: ./openapi.yaml
# → Output directory: ./sdks
# → Writing stainless.config.yaml...
# → Done. Run `stainless generate` to produce your first SDK.
The generated stainless.config.yaml looks like this:
# stainless.config.yaml
version: 1
project: my-claude-sdk
spec:
path: ./openapi.yaml # Path to your OpenAPI 3.1 spec
targets:
typescript:
output: ./sdks/typescript
package_name: my-claude-sdk
runtime: node # Options: node | deno | browser
python:
output: ./sdks/python
package_name: my_claude_sdk # PEP-8 snake_case enforced automatically
python_version: "3.11"
retry:
max_retries: 2 # Default retry count applied to all targets
retry_on: [429, 500, 502, 503]
Note: The
projectname must match your Stainless workspace project slug exactly, or thegeneratecommand will return a 404 when it attempts to pull remote configuration.
Step 2 — Prepare and Validate Your OpenAPI Specification
The quality of your output SDK is directly proportional to the quality of your OpenAPI spec. Stainless can infer a lot, but it can't invent information that isn't there — missing operationId fields and under-specified response schemas are the two most common sources of ugly generated code.
Structuring operationId Fields for Clean Method Names
Stainless derives SDK method names from operationId. The convention it expects is resource_action format (e.g., messages_create, messages_list). Stainless maps these to client.messages.create() and client.messages.list() — the same structure used in the official Claude SDK.
Adding x-stainless Extension Fields
The x-stainless-pagination and x-stainless-retry extensions give the generator the behavioral metadata it needs to produce correct streaming and retry logic without you writing a line of transport code.
# openapi.yaml — OpenAPI 3.1 spec for a Claude-style /v1/messages endpoint
openapi: "3.1.0"
info:
title: My Claude-Compatible API
version: "1.0.0"
paths:
/v1/messages:
post:
operationId: messages_create # maps to client.messages.create()
summary: Create a message
x-stainless-retry: # override global retry settings per-endpoint
max_retries: 3
retry_on: [429, 500, 503]
x-stainless-streaming: true # tells Stainless to generate a streaming variant
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MessageCreateParams"
responses:
"200":
description: Successful message response
content:
application/json:
schema:
$ref: "#/components/schemas/Message"
/v1/messages/{message_id}:
get:
operationId: messages_retrieve # maps to client.messages.retrieve(id)
summary: Retrieve a message by ID
parameters:
- name: message_id
in: path
required: true
schema:
type: string
responses:
"200":
description: Message object
content:
application/json:
schema:
$ref: "#/components/schemas/Message"
components:
schemas:
MessageCreateParams:
type: object
required: [model, messages]
properties:
model:
type: string
example: claude-opus-4-8
max_tokens:
type: integer
minimum: 1
maximum: 32768
messages:
type: array
items:
$ref: "#/components/schemas/MessageParam"
stream:
type: boolean
default: false
MessageParam:
type: object
required: [role, content]
properties:
role:
type: string
enum: [user, assistant]
content:
type: string
Message:
type: object
properties:
id:
type: string
type:
type: string
enum: [message]
role:
type: string
content:
type: array
items:
type: object
model:
type: string
stop_reason:
type: string
nullable: true
usage:
$ref: "#/components/schemas/Usage"
Usage:
type: object
properties:
input_tokens:
type: integer
output_tokens:
type: integer
Linting Your Spec Before Generation
stainless validate ./openapi.yaml
# → ✓ OpenAPI version: 3.1.0
# → ✓ All operations have operationId
# → ✓ x-stainless extensions validated
# → ✓ No missing $ref targets
# → Spec is valid. Ready to generate.
Fix every warning before proceeding — warnings become broken SDK methods.
Step 3 — Generate SDKs Across TypeScript and Python
With a validated spec and configured stainless.config.yaml, you're ready to generate. Each stainless generate invocation calls the Stainless API, applies your config, and writes idiomatic SDK code to your output directories.
Running stainless generate for TypeScript Output
stainless generate --target typescript
# → Fetching remote config for my-claude-sdk...
# → Generating TypeScript SDK...
# → Writing to ./sdks/typescript
# → Done in 4.2s
Running stainless generate for Python Output
stainless generate --target python
# → Generating Python SDK...
# → Writing to ./sdks/python
# → Done in 3.8s
Reviewing the Generated Client Constructors
Here's the same client constructor generated in both languages from the spec above. The parallel structure mirrors exactly what you'd find in the official anthropic-sdk-typescript and anthropic-sdk-python repositories — because Stainless generated those too.
// sdks/typescript/src/index.ts — Generated TypeScript client
import { MyClaude } from './client';
// Constructor with full type safety
const client = new MyClaude({
apiKey: process.env['MY_CLAUDE_API_KEY'], // auto-read from env if omitted
maxRetries: 2,
timeout: 60_000, // ms
});
// Typed async/await method — MessageCreateParams is a generated interface
const message = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello, Claude.' }],
});
console.log(message.content);
// Streaming variant — generated because x-stainless-streaming: true
const stream = await client.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Stream this.' }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.delta?.text ?? '');
}
# sdks/python/src/my_claude_sdk/client.py — Generated Python client
import asyncio
import os
from my_claude_sdk import AsyncMyClaude
from my_claude_sdk.types import MessageCreateParams
async def main() -> None:
# Constructor mirrors TS: env var fallback, retry config, timeout
client = AsyncMyClaude(
api_key=os.environ.get("MY_CLAUDE_API_KEY"),
max_retries=2,
timeout=60.0, # seconds
)
# Pydantic-backed typed params — IDE autocomplete works out of the box
message = await client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude."}],
)
print(message.content)
# Streaming with async context manager — generated from x-stainless-streaming
async with client.messages.stream(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": "Stream this."}],
) as stream:
async for text in stream.text_stream:
print(text, end="", flush=True)
asyncio.run(main())
Note: You do not need to write transport logic, retry loops, or streaming parsers. Stainless generates all of that from your spec annotations. The generated code is not a thin wrapper — it includes connection pooling, exponential backoff, header injection, and proper error class hierarchies.
Step 4 — Generate an MCP Server for Claude Agent Connectivity
MCP (Model Context Protocol) is the open standard Anthropic created to give Claude agents structured, typed access to external tools and data sources. Instead of Claude calling arbitrary HTTP endpoints, it calls registered MCP tools that have well-defined input schemas — which means safer, more reliable agent behavior.
What MCP Servers Are and Why Anthropic Created the Protocol
An MCP server exposes a set of named tools with typed parameters to an AI agent runtime. Claude's agent loop can discover available tools, reason about which one to call, and invoke it with structured arguments — all without the agent needing to parse raw HTTP responses. The acquisition of Stainless makes sense here: Stainless can generate an MCP server directly from the same OpenAPI spec that produces your REST SDK, giving you both human-developer and agent-developer surfaces from a single source of truth.
Using stainless generate --target mcp to Scaffold an MCP Server
stainless generate --target mcp
# → Generating MCP server...
# → Writing to ./sdks/mcp-server
# → Registered 2 tools from spec: messages_create, messages_retrieve
# → Done in 2.1s
The Generated MCP Server Scaffold
// sdks/mcp-server/index.ts — Generated MCP server
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import MyClaude from '../typescript/src/index.js';
const server = new McpServer({
name: 'my-claude-sdk-mcp',
version: '1.0.0',
});
const apiClient = new MyClaude({
apiKey: process.env['MY_CLAUDE_API_KEY'],
});
// Tool: messages_create — schema generated directly from OpenAPI requestBody
server.tool(
'messages_create',
'Create a new message using the Claude-compatible API',
{
model: z.string().describe('Model identifier, e.g. claude-opus-4-8'),
max_tokens: z.number().int().min(1).max(32768).optional(),
messages: z.array(
z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
})
),
},
async ({ model, max_tokens, messages }) => {
const response = await apiClient.messages.create({
model,
max_tokens: max_tokens ?? 1024,
messages,
});
return {
content: [{
type: 'text',
text: JSON.stringify(response.content),
}],
};
}
);
// Tool: messages_retrieve — schema from OpenAPI path parameters
server.tool(
'messages_retrieve',
'Retrieve a previously created message by ID',
{
message_id: z.string().describe('The unique message identifier'),
},
async ({ message_id }) => {
const message = await apiClient.messages.retrieve(message_id);
return {
content: [{
type: 'text',
text: JSON.stringify(message),
}],
};
}
);
// Start the server on stdio transport (compatible with Claude Desktop and agent runtimes)
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server running on stdio');
To register this with a Claude agent or Claude Desktop, add it to your claude_desktop_config.json:
{
"mcpServers": {
"my-claude-sdk": {
"command": "node",
"args": ["./sdks/mcp-server/index.js"],
"env": {
"MY_CLAUDE_API_KEY": "your-api-key-here"
}
}
}
}
Note: The Stainless acquisition rationale is visible here: agents are only as capable as the systems they can reach. By generating an MCP server from the same spec that produces your SDK, every API you own becomes immediately accessible to Claude agents without writing a single tool registration by hand.
Common Issues & Fixes
Error: operationId is missing on POST /v1/messages — Validation halts SDK generation
Stainless requires every path operation to have a unique operationId. Without it, the generator cannot produce a method name and aborts. This is the most common first-time error.
Before (broken):
paths:
/v1/messages:
post:
summary: Create a message
# operationId is missing — Stainless will error here
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MessageCreateParams"
After (fixed):
paths:
/v1/messages:
post:
operationId: messages_create # Added: maps to client.messages.create()
summary: Create a message
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MessageCreateParams"
Run stainless validate ./openapi.yaml after fixing to confirm the error is resolved before re-running generate.
Error: AuthenticationError: 401 Unauthorized — Generated Python client fails against Claude API
This happens when api_key is None at runtime because the environment variable name in your generated client doesn't match what you've set in your shell. Stainless derives the env var name from your package_name in stainless.config.yaml. If your package is my_claude_sdk, the client looks for MY_CLAUDE_SDK_API_KEY.
# Explicit key injection bypasses env var lookup entirely — useful for debugging
client = AsyncMyClaude(api_key="sk-ant-xxxx") # confirm the key works
# Then fix your shell env to use the correct auto-derived name:
# export MY_CLAUDE_SDK_API_KEY="sk-ant-xxxx"
Check the generated client source at sdks/python/src/my_claude_sdk/_client.py and search for environ.get to see the exact env var name the client expects.
Error: Module '"my-claude-sdk"' has no exported member 'MessageCreateParams' — TypeScript types not exported
This happens when your tsconfig.json exports map doesn't include the types condition, or when the generated index.d.ts is not in the path the consumer's TypeScript resolver expects.
Broken package.json exports map:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
Fixed package.json exports map:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"types": "./dist/index.d.ts"
}
Also confirm your tsconfig.json sets "moduleResolution": "bundler" or "node16" — the legacy "node" resolver ignores the exports field entirely and will silently fail to find type declarations.
FAQ
Q: Does the Anthropic acquisition of Stainless change existing Stainless SDK pricing or plans?
As of the acquisition announcement in May 2026, Anthropic has not announced changes to Stainless's pricing or self-serve plans for third-party customers. The official statement emphasized that the Stainless team continues doing the same work on the same platform. If you're an existing Stainless customer generating SDKs for non-Anthropic APIs, your setup should continue working without modification — but monitor the Stainless changelog for any transition announcements over the following months.
Q: Can I use Stainless to generate SDKs for APIs other than Anthropic Claude?
Absolutely — hundreds of companies were using Stainless for their own APIs long before the Anthropic acquisition. Stainless is a general-purpose SDK generator that works with any valid OpenAPI 3.x spec. Companies use it to generate SDKs for payment APIs, infrastructure APIs, database APIs, and more. The Anthropic angle in this tutorial just means the toolchain is proven against a high-traffic, production API that processes millions of requests. The same stainless.config.yaml and CLI commands apply to any API spec you own.
Q: How does a Stainless-generated SDK differ from writing an SDK wrapper manually?
A manually written SDK wrapper typically covers the happy path for each endpoint and little else. Stainless-generated SDKs include retry logic with exponential backoff, streaming support where specified, type-safe error class hierarchies, pagination helpers, automatic header injection, connection pooling, and idiomatic async patterns — all per language. Maintaining that across TypeScript, Python, Go, and Java simultaneously, while keeping them in sync as your API evolves, is weeks of engineering work per SDK version. Stainless regenerates all of it in seconds from a single spec change.