Private beta. Public soon.
How it works

The hard parts, handled.

Generating a tool from docs isn't just "read page, output code." One endpoint can span 10 doc pages with 200 nested schemas. Miss one enum, one casing rule, one input-only field — the tool breaks. R28 handles all of it.

01 — Generation

docs2tool.
Harder than it looks.

1. Crawl

Follows every link across doc pages. Discovers all nested types, enums, and transitive dependencies.

2. Extract

Pulls schema definitions from noisy doc pages. Not regex — semantic understanding.

3. Verify

Automated checks catch bad extractions before they make it to code.

4. Generate

Outputs strictly typed models with the same constraints as the docs.

One endpoint, four pages

R28 crawls every linked doc page, extracts all schema definitions, resolves the full dependency graph, and generates typed tools with every field, enum, and nested type intact.

POST /v1/charges
  └─ ChargeCreateInput
       ├─ amount: int
       ├─ currency: str (required)
       ├─ customer: str (optional)
       └─ source: Source    ← page 2
            ├─ type: SourceType ← page 3 (enum)
            │    ├─ "card"
            │    ├─ "bank_account"
            │    └─ "ach_debit"
            └─ card: CardParams  ← page 4
                 ├─ number: str
                 ├─ exp_month: int
                 ├─ exp_year: int
                 └─ cvc: str
02 — Accuracy

Zero drift from the docs

A 1:1 projection of the API documentation — not an approximation, not a "good enough" guess.

Auth

OAuth scopes, credential providers — configured once at the factory level, inherited by every tool.

URL templates

Path parameters like {calendarId} are extracted from Path() fields automatically.

Body routing

Path(), Query(), Body() — each field knows where it goes in the HTTP request.

Mode filtering

Mode("response_only") strips fields the LLM can't set. The schema stays complete — the LLM just never sees the noise.

Key casing

body_case, query_case — the LLM writes snake_case, the API gets camelCase. Per-location, per-field overrides when needed.

# ── client.py ──

calendar_tool = api_tool_factory(
    base_url="https://googleapis.com/",
    credential=GoogleCredentialProvider,
    scope=GoogleScopes.CALENDAR,
    body_case="camel",
    query_case="camel",
)

# ── tools.py ──

events_insert = calendar_tool(
    name="events_insert",
    method="POST",
    url_template=
      "calendar/v3/calendars/{calendarId}/events",
    args_schema=EventsInsertRequest,
)

# ── models.py ──

class EventsInsertRequest:
    calendar_id:   str,          Path()
    send_updates:  str | None,   Query()

    summary:       str,          Body()
    start:         DateTime,     Body()
    end:           DateTime,     Body()
    attendees:     [Attendee],  Body()

    id:             str,          Mode("response_only")
    status:         str,          Mode("response_only")
03 — Format bridge

The LLM sees what it needs.
The API gets what it expects.

Every transformation — formats, casing, field filtering — is handled in the middleware, invisible to both sides.

Gmail
{ "raw": "dG86IGFsaWNlQG V4YW1wbGUuY29t Ck1JTUUtVmVyc2l ..." }
EmailContent { to: "alice@example.com" subject: "Hello" body: "Hi Alice!" }
How R28 bridges it
Format("rfc822_base64") * Builds MIME message * Encodes to base64url * Wraps in {"raw": "..."}
Google Sheets
{ "values": [ [{"stringValue":"Name"}, {"stringValue":"Age"}], [{"stringValue":"Alice"}, {"numberValue":30}] ] }
values: [ ["Name", "Age"], ["Alice", 30], ["Bob", 25] ]
How R28 bridges it
Format("proto_json") * Converts plain JSON to protobuf Value type * Wraps each cell
04 — Context management

Response transforms,
not raw dumps.

A single raw Gmail thread can be 50KB+. That's more than simply noise — it's a context window bomb. With R28 you can transform responses before the LLM ever sees them to keep your context window clean.

What the LLM actually gets

Clean, readable text. No MIME, no base64, no tracking pixels, no 40+ headers. Just the content that matters.

{"payload":{"mimeType":"multipart/mixed","parts":[{"mimeType":"text/plain","body":{"data":"SGVsbG8gQWxpY2Uh..."}},{"mimeType":"text/html","body":{"data":"PGh0bWw+PGhlYWQ+PHN0eWxlPi5FeHRl...tracking pixel..."}},{"mimeType":"application/pdf","filename":"invoice.pdf","body":{"attachmentId":"ANGjdJ..."}}],"headers":[{"name":"From","value":"bob@..."},{"name":"Date","value":"Thu, 27..."},{"name":"X-Mailer","value":"..."},{"name":"DKIM-Signature","value":"v=1;a=rsa-sha256;c=relaxed/relaxed;d=example.com;s=..."},{"name":"Received","value":"from mx.google.com..."},...40+ more headers]}}

From: bob@example.com

To: alice@example.com

Date: Thu, 27 Mar 2026

Subject: Invoice for March

Hello Alice!

Please find attached the invoice for March. Let me know if you have any questions.

Best,
Bob

Attachments: invoice.pdf (application/pdf)

Generate your first MCP server