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.
"""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() # --- 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 functionsimport 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.