Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started
Spec → SDKs · Docs · MCP

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.

You write

paths:
  /pets:
    get:
      operationId: listPets
      x-pagination:
        strategy: cursor
        cursorParam: cursor
        itemsPath: data
        nextCursorPath: next_cursor

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

The wedge

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.

The output

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.

How it works

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.

  1. 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.

  2. 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.

  3. Codegen

    Every declared target emits at once — SDKs across 13 languages, your docs site, and the MCP server — from that single IR.

  4. 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.

  5. 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.

  6. 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
```
regenerated + byte-diffed in CI renderDriftReport (@glotto/core-vcs) packages/cli/tests/fixtures/drift-gate 0560947c783e
Switching costs, near zero

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.