production

From gist
to service.

A gist is a plain Python file. The same file runs in repld while you and Claude prototype, then in FastMCP or FastAPI once it ships. You swap the few lines that connect it. This page shows how.

Core logic stays.
Wiring gets replaced.

Write gists in two layers. The core logic sits at the top and moves to production untouched. The repld wiring sits at the bottom: a thin _tool_* function with type hints — repld infers the schema, no separate declaration needed. That bottom layer drops away when the gist graduates, and a @mcp.tool or @router.get takes its place.

keeps
"""acme — client for the Acme directory."""

import os
import httpx

# --- core logic (portable — keeps on graduation) ---

async def lookup(company_id: str) -> dict:
    """Look up a company. -> {name, address, ...}"""
    async with httpx.AsyncClient() as c:
        resp = await c.get(
            f"https://api.example.com/company/{company_id}",
            headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
        )
        return resp.json()
sheds
# --- repld wiring (shed on graduation) ---
# _tool_ prefix + type hints is all repld needs — schema is inferred,
# no separate declaration or manual (de)serialization required.

async def _tool_lookup(company_id: str) -> dict:
    """Look up a company."""
    return await lookup(company_id)

The browser is the credential.

Some APIs only authenticate through your logged-in browser session, with no API key to hand out. For those, accept a fetch= callable. In repld you pass tab.fetch, which rides your live session. In production you pass an httpx client or switch to a token. The core function reads the same data either way.

# Browser-auth variant — accept a fetch callable
# Works with browser session (no token) AND standalone (token from env)

async def lookup(company_id: str, *, fetch=None) -> dict:
    """Look up a company. -> {name, address, ...}"""
    if fetch is not None:
        return (await fetch(f"/api/company/{company_id}"))["body"]
    async with httpx.AsyncClient() as c:
        resp = await c.get(
            f"https://api.example.com/company/{company_id}",
            headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
        )
        return resp.json()

One .env,
both contexts.

Core logic reads tokens from os.environ, so nothing is hardcoded. repld loads .env from the project root when the session starts. Your production framework loads the same file its own way. The gist code stays identical.

# .env at project root — loaded by repld at boot,
# by FastAPI/FastMCP in production. Same file, both contexts.

API_TOKEN=sk-live-abc123
ACME_API_KEY=xyz789
DATABASE_URL=postgresql://localhost/mydb

Pick the tier that fits.

Gists don't all need the same production setup. Some hit public APIs with a token. Others need your browser's authenticated session. A few want both.

Tier Auth repld dep? When
Standalone .env tokens No Stable API, you have keys
Browser-backed browser.fetch() repld-tool[browser] Session-auth APIs, no proper keys
Hybrid Token + browser fallback Optional Token when available, browser otherwise

One line to
an MCP tool.

mcp.add_tool() reads the input schema off your type hints and the description off the docstring. If the core function's signature already fits, that one line is the whole integration.

from fastmcp import FastMCP
from gists.acme import lookup

mcp = FastMCP("my-service")

# One line — schema generated from type hints + docstring
mcp.add_tool(lookup)
# When you need a different name or adapted parameters:
@mcp.tool
async def company_lookup(company_id: str) -> dict:
    """Look up a company."""
    return await lookup(company_id)

Same function,
HTTP endpoint.

Wrap the core function in a route decorator and FastAPI takes it from there: request validation, JSON serialization, OpenAPI docs. The gist itself doesn't change.

from fastapi import APIRouter
from gists.acme import lookup

router = APIRouter()

@router.get("/company/{company_id}")
async def company_lookup(company_id: str):
    return await lookup(company_id)

Seven commands to a running service.

Create a project, add the dependencies, copy the gist in, run it. The _tool_* layer stays behind. A @mcp.tool or @router.get replaces it.

$ uv init my-service && cd my-service

# Add framework
$ uv add fastmcp                # or: uv add fastapi uvicorn

# Add gist deps (from __repld_deps__)
$ uv add httpx

# If browser-backed:
$ uv add repld-tool[browser]

# Copy gists (vendor them)
$ mkdir -p gists
$ cp ~/other-project/gists/acme.py gists/

# Run
$ uv run fastmcp run server.py  # FastMCP
$ uv run uvicorn main:app       # FastAPI

What moves, what stays put.

Stays (portable)

  • Core async functions
  • Type hints + docstrings
  • os.environ["TOKEN"]
  • Data parsing helpers
  • __repld_deps__ (as reference)

Goes (repld-specific)

  • _tool_* handler functions
  • import repld (in wiring)
  • tab.fetch / browser.get
  • __repld_usage__

Say this when you're ready.

Claude reads the production guide on demand. One line gives it the context for the whole graduation conversation.

Starting the conversation

Read repld://docs/production — let's graduate this gist

Standalone service

Set up a FastMCP server for this gist — no browser needed

Browser-backed service

Graduate this to FastAPI with repld-tool[browser]

The same file carries you from discovery to production. You and Claude prototype live: poking at APIs, reverse-engineering the traffic, shaping the data into clean Python. Once it works, you copy the file, swap the few lines that connect it, and run it headless. Nothing gets rewritten. The progression doesn't stop at the REPL. It ends at a running service.