Spec in. Best-in-class SDKs, docs, and an MCP server — out.
Glotto keeps your entire developer surface — idiomatic SDKs in 13 languages (mobile-first), a docs site you own, and a multi-mode MCP server — provably correct and in lockstep with your spec, forever.
Glotto emits
Auto-paginated, typed, retried — in the idiom of each language.
import { Client } from 'glotto-sdk';
const client = new Client({ token: '<token>' });
for await (const item of client.pets.listPets()) {
console.log(item);
}regenerated + byte-diffed in CI a0f8af4b462c
Built for what the others skip
Where Stainless, Speakeasy, and Fern stop, Glotto keeps going.
Machine-guaranteed correctness
Every regeneration is compile-verified and contract-tested across each language, drift-gated, and preserves your custom code — provably in lockstep with your spec.
Mobile-first SDKs
A React Native / Expo spearhead plus Kotlin Multiplatform and native Android/iOS — the mobile targets no rival ships first-class.
Deploy on your terms
Self-host or single-tenant, with managed release PRs across GitHub, GitLab, and Bitbucket — not a GitHub-cloud-only box.
Three things, from one spec
Every push regenerates all three, in lockstep with your API.
SDKs
13 languages, mobile-first, with a hand-crafted feel and machine-guaranteed correctness — not generated boilerplate.
Docs
A standalone Astro docs site you own — multi-language snippets, search, and an MCP-ready index.
MCP server
Dual-mode (Code Mode + per-operation) so AI agents can drive your API safely.
Push a spec. Merge to publish. Then keep being right.
Five steps take your spec to published SDKs. The sixth is the one that matters: every push after the first re-runs the whole pipeline, proves the result still compiles and still honours your contract in every language, and preserves the code you wrote by hand.
-
API spec
Point Glotto at an OpenAPI, AsyncAPI, or GraphQL document — a file, a URL, a git ref in another repo, or your own exporter command.
-
Glotto IR
Your spec normalizes to one canonical, byte-stable intermediate representation, so every language generates from the same reading of your API rather than from its own.
-
Codegen
Every declared target emits at once — SDKs across 13 languages, your docs site, and the MCP server — from that single IR.
-
Verify
Before anything opens, each SDK is compiled in its own real toolchain and run against a shared contract suite, so "it generated" and "it works" are not the same claim.
-
Release PR
A reviewable pull request lands on GitHub, GitLab, or Bitbucket — your repos, your review, your history. Nothing is force-pushed and nothing publishes itself.
-
Every push after
The loop. Regeneration is drift-gated against the committed output, your hand-written code survives via a three-way merge, and a breaking change is reported before it ships, not after.
What Codegen emits — authentication, in every language
import { Client } from 'glotto-sdk';
const client = new Client({ token: '<token>' });
const result = await client.pets.createPet({ name: 'Biscuit', species: 'cat' });regenerated + byte-diffed in CI a0f8af4b462c
What Verify catches — drift between your spec and the committed SDK
one word changed in the spec — `summary: List pets` became `summary: List every pet`
- files compared 81
- out of sync 11
## SDK drift detected
11 files out of sync with the committed SDK.
### changed: `cli/README.md`
```diff
- | `pets listPets` | `GET /pets` | List pets |
+ | `pets listPets` | `GET /pets` | List every pet |
```
### changed: `cli/cli.js`
```diff
- "summary": "List pets",
+ "summary": "List every pet",
```
### changed: `graph/graph.json`
```diff
- "summary": "List pets"
+ "summary": "List every pet"
```
### changed: `mcp/src/server.ts`
```diff
- // @glotto:generated-checksum e30acd0f04fffbf5e8122b85c3c76c9dbe50cece7ff42c7e47803ef9227fb641
- // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
- import { McpServer } from '@modelcontextprotocol/server';
- import { z } from 'zod';
- import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
- import { registerDynamicTools, registerOperationTools } from './dynamic.js';
- import type { ServerDescription } from './debug.js';
- import { parseFilters, shouldRegister } from './filters.js';
- import { parseMode, type ServerMode } from './mode.js';
- import { browseOperations, type OperationEntry } from './operations.js';
- import { forwardedCredentialSource } from './passthrough.js';
- import { clampText, errorResult, projectText } from './results.js';
- import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
- import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
- import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
-
- const SERVER_NAME = "petstore-mcp";
- const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
- const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
- // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
- const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
-
- // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
- const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
-
- const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
- "Pet": {
- "kind": "object",
- "fields": {
- "id": {
- "kind": "integer"
- },
- "name": {
- "kind": "string"
- }
- },
- "required": [
- "id",
- "name"
- ]
- }
- };
- const SEARCH_INDEX_BASELINE: SearchRecord[] = [
- {
- "kind": "reference",
- "title": "Create a pet",
- "url": "/reference/pets/createPet",
- "resource": "pets",
- "method": "createPet",
- "httpMethod": "POST",
- "path": "/pets",
- "tool": "pets_createPet",
- "sdkCall": "client.pets.createPet",
- "parameters": [],
- "text": "Create a pet",
- "requestBody": {
- "required": true,
- "contentType": "application/json",
- "schema": {
- "kind": "model",
- "model": "Pet"
- }
- },
- "response": {
- "kind": "model",
- "model": "Pet"
- },
- "sampleInput": {
- "id": 0,
- "name": "<name>"
- }
- },
- {
- "kind": "reference",
- "title": "List pets",
- "url": "/reference/pets/listPets",
- "resource": "pets",
- "method": "listPets",
- "httpMethod": "GET",
- "path": "/pets",
- "tool": "pets_listPets",
- "sdkCall": "client.pets.listPets",
- "parameters": [],
- "text": "List pets",
- "response": {
- "kind": "array",
- "items": {
- "kind": "model",
- "model": "Pet"
- }
- }
- }
- ];
- const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
- // Loaded once at startup; the search_docs handler awaits this cached merged index.
- const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
-
- // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
- // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
- // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
- // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
- // credential HERE, per request, so the guest's client and the Tools Mode handlers can
- // never disagree about which credential this caller reaches the API with.
- function applyAuth(headers: Record<string, string>): void {
- const forwarded = forwardedCredentialSource();
- const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
- if (token) headers['Authorization'] = "Bearer " + token;
- }
-
- // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
- // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
- // through the list_tools/describe_tools/invoke_tool meta-tools.
- const OPERATIONS: readonly OperationEntry[] = [
- {
- name: "pets_createPet",
- description: "Create a pet",
- resource: "pets",
- method: "createPet",
- httpMethod: "POST",
- path: "/pets",
- meta: { resource: "pets", tags: ["pets"] },
- jqInjected: true,
- inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
- outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
- handler: async (input, jqFilter) => {
- const path = "/pets";
- const url = new URL(BASE_URL + path);
- const headers: Record<string, string> = {};
- headers['content-type'] = 'application/json';
- applyAuth(headers);
- const response = await fetch(url, {
- method: "POST",
- headers,
- body: JSON.stringify(input['body']),
- });
- const text = await response.text();
- if (!response.ok) return errorResult(response.status, text);
- let structured: unknown;
- try {
- structured = JSON.parse(text);
- } catch {
- structured = undefined;
- }
- if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
- return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
- }
- const projected = projectText(text, jqFilter);
- if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
- return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
- },
- },
- {
- name: "pets_listPets",
- description: "List pets",
+ // @glotto:generated-checksum 53232a1a568b12e4d6b606365ec19a3c1375664757cc1611e9edecef1a193b61
+ // Generated by @glotto/codegen-mcp. Edit glotto.yml, not this file.
+ import { McpServer } from '@modelcontextprotocol/server';
+ import { z } from 'zod';
+ import { type ClientCapabilities, jsonCoercer, nameAdapter, parseCapabilities, type SchemaAdapter, schemaAdapter } from './adapt.js';
+ import { registerDynamicTools, registerOperationTools } from './dynamic.js';
+ import type { ServerDescription } from './debug.js';
+ import { parseFilters, shouldRegister } from './filters.js';
+ import { parseMode, type ServerMode } from './mode.js';
+ import { browseOperations, type OperationEntry } from './operations.js';
+ import { forwardedCredentialSource } from './passthrough.js';
+ import { clampText, errorResult, projectText } from './results.js';
+ import { createSandbox, type SandboxSdkBinding } from './sandbox.js';
+ import { SDK_ENTRYPOINT, SDK_FILES } from './sdk-source.js';
+ import { buildMergedIndex, resolveResultModels, searchDocsPage, type SearchRecord, type TypeDescriptor } from './search.js';
+
+ const SERVER_NAME = "petstore-mcp";
+ const BASE_URL = process.env['GLOTTO_API_BASE_URL'] ?? "https://api.petstore.example";
+ const SANDBOX_PERMISSIONS = { allowNet: ["api.petstore.example"] };
+ // glotto.yml#/mcp#/modes: a requested-but-disabled mode resolves to the enabled default.
+ const ENABLED_MODES: readonly ServerMode[] = ["tools","code","dynamic"];
+
+ // Named models referenced by the tool schemas; z.lazy so reference cycles resolve.
+ const Model_Pet: z.ZodTypeAny = z.lazy(() => z.object({ "id": z.number().int(), "name": z.string() }));
+
+ const SEARCH_INDEX_MODELS: Record<string, TypeDescriptor> = {
+ "Pet": {
+ "kind": "object",
+ "fields": {
+ "id": {
+ "kind": "integer"
+ },
+ "name": {
+ "kind": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ]
+ }
+ };
+ const SEARCH_INDEX_BASELINE: SearchRecord[] = [
+ {
+ "kind": "reference",
+ "title": "Create a pet",
+ "url": "/reference/pets/createPet",
+ "resource": "pets",
+ "method": "createPet",
+ "httpMethod": "POST",
+ "path": "/pets",
+ "tool": "pets_createPet",
+ "sdkCall": "client.pets.createPet",
+ "parameters": [],
+ "text": "Create a pet",
+ "requestBody": {
+ "required": true,
+ "contentType": "application/json",
+ "schema": {
+ "kind": "model",
+ "model": "Pet"
+ }
+ },
+ "response": {
+ "kind": "model",
+ "model": "Pet"
+ },
+ "sampleInput": {
+ "id": 0,
+ "name": "<name>"
+ }
+ },
+ {
+ "kind": "reference",
+ "title": "List every pet",
+ "url": "/reference/pets/listPets",
+ "resource": "pets",
+ "method": "listPets",
+ "httpMethod": "GET",
+ "path": "/pets",
+ "tool": "pets_listPets",
+ "sdkCall": "client.pets.listPets",
+ "parameters": [],
+ "text": "List every pet",
+ "response": {
+ "kind": "array",
+ "items": {
+ "kind": "model",
+ "model": "Pet"
+ }
+ }
+ }
+ ];
+ const DOCS_INDEX_LOCATION = process.env['GLOTTO_DOCS_INDEX'];
+ // Loaded once at startup; the search_docs handler awaits this cached merged index.
+ const docsIndexPromise = buildMergedIndex(SEARCH_INDEX_BASELINE, DOCS_INDEX_LOCATION);
+
+ // Attach the upstream API credential to each outbound Tools Mode request: the caller-forwarded
+ // credential when credential passthrough is armed (mcp-credential-passthrough #3350), else this
+ // caller's own upstream OAuth token when that flow is on (#3351), else the operator's
+ // environment. Code Mode reads this too (#3348): its guest↔SDK binding resolves the
+ // credential HERE, per request, so the guest's client and the Tools Mode handlers can
+ // never disagree about which credential this caller reaches the API with.
+ function applyAuth(headers: Record<string, string>): void {
+ const forwarded = forwardedCredentialSource();
+ const token = forwarded !== undefined ? forwarded.authorization : process.env["GLOTTO_API_TOKEN"];
+ if (token) headers['Authorization'] = "Bearer " + token;
+ }
+
+ // The shared operation table (mcp-dynamic-tools-mode, ADR-0088), in collectOperations order:
+ // Tools Mode registers each entry as its own tool; Dynamic Mode serves the same entries
+ // through the list_tools/describe_tools/invoke_tool meta-tools.
+ const OPERATIONS: readonly OperationEntry[] = [
+ {
+ name: "pets_createPet",
+ description: "Create a pet",
+ resource: "pets",
+ method: "createPet",
+ httpMethod: "POST",
+ path: "/pets",
+ meta: { resource: "pets", tags: ["pets"] },
+ jqInjected: true,
+ inputShape: (coerce) => ({ body: coerce(Model_Pet), jq_filter: z.string().describe("jq-style filter to shape the text result (subset: .field, .[\"key\"], .[0], .[] iteration, | pipes); applied to the JSON response body").optional() }),
+ outputSchema: z.object({ "id": z.number().int(), "name": z.string() }),
+ handler: async (input, jqFilter) => {
+ const path = "/pets";
+ const url = new URL(BASE_URL + path);
+ const headers: Record<string, string> = {};
+ headers['content-type'] = 'application/json';
+ applyAuth(headers);
+ const response = await fetch(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(input['body']),
+ });
+ const text = await response.text();
+ if (!response.ok) return errorResult(response.status, text);
+ let structured: unknown;
+ try {
+ structured = JSON.parse(text);
+ } catch {
+ structured = undefined;
+ }
+ if (typeof structured !== 'object' || structured === null || Array.isArray(structured)) {
+ return { content: [{ type: 'text', text: 'HTTP ' + response.status + ': expected a JSON object response body but received: ' + (text === '' ? 'an empty body' : text) }], isError: true };
+ }
+ const projected = projectText(text, jqFilter);
+ if (!projected.ok) return { content: [{ type: 'text', text: projected.text }], isError: true };
+ return { content: [{ type: 'text', text: clampText(projected.text) }], structuredContent: structured as Record<string, unknown> };
+ },
+ },
+ {
+ name: "pets_listPets",
+ description: "List every pet",
```
### changed: `openapi.decorated.json`
```diff
- "summary": "List pets",
+ "summary": "List every pet",
```
### changed: `spec_repo/spec.base.json`
```diff
- "summary": "List pets",
+ "summary": "List every pet",
```
### changed: `spec_repo/spec.base.yaml`
```diff
- summary: List pets
+ summary: List every pet
```
### changed: `spec_repo/spec.with-code-samples.json`
```diff
- "summary": "List pets",
+ "summary": "List every pet",
```
### changed: `spec_repo/spec.with-code-samples.yaml`
```diff
- summary: List pets
+ summary: List every pet
```
### changed: `spec_repo/spec.with-transforms.json`
```diff
- "summary": "List pets",
+ "summary": "List every pet",
```
### changed: `spec_repo/spec.with-transforms.yaml`
```diff
- summary: List pets
+ summary: List every pet
``` renderDriftReport (@glotto/core-vcs) packages/cli/tests/fixtures/drift-gate 0560947c783e Migrating from Stainless?
Stainless is winding down its hosted platform. Glotto picks up where it leaves off — a superset of its SDK languages, the same Astro docs approach, and a multi-mode MCP server including Code Mode and dynamic tools. One command converts your stainless.yml.
Ship the SDKs your users deserve.
One spec. 13 languages. Docs and MCP included.