{"id":47681631,"url":"https://github.com/brunvelop/refract","last_synced_at":"2026-04-02T14:01:21.092Z","repository":{"id":346976163,"uuid":"1189850603","full_name":"Brunvelop/refract","owner":"Brunvelop","description":"One Python function → REST API, CLI, MCP tools for AI agents \u0026 Web UI. Zero boilerplate.","archived":false,"fork":false,"pushed_at":"2026-03-26T16:04:55.000Z","size":174,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-27T02:35:26.108Z","etag":null,"topics":["ai-agents","cli","fastapi","llm-tools","mcp","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Brunvelop.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-23T18:20:30.000Z","updated_at":"2026-03-26T16:05:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Brunvelop/refract","commit_stats":null,"previous_names":["brunvelop/refract"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Brunvelop/refract","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brunvelop%2Frefract","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brunvelop%2Frefract/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brunvelop%2Frefract/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brunvelop%2Frefract/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Brunvelop","download_url":"https://codeload.github.com/Brunvelop/refract/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Brunvelop%2Frefract/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31307459,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","cli","fastapi","llm-tools","mcp","python"],"created_at":"2026-04-02T14:00:40.262Z","updated_at":"2026-04-02T14:01:21.075Z","avatar_url":"https://github.com/Brunvelop.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# refract 💎\n\n\u003e **One Python function. Four interfaces. Zero boilerplate.**\n\n[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Status: Stable](https://img.shields.io/badge/status-stable-brightgreen.svg)]()\n\nDefine a typed Python function once — and automatically get a **REST API**, a **CLI**, **MCP tools** for AI agents, and a **Frontend SDK**.\n\n```python\nfrom pydantic import BaseModel\nfrom refract import Refract, register_function\n\nclass Sum(BaseModel):\n    result: int\n\n@register_function()\ndef add(a: int, b: int) -\u003e Sum:\n    \"\"\"Add two numbers.\n    Args:\n        a: First number\n        b: Second number\n    \"\"\"\n    return Sum(result=a + b)\n\napp = Refract(\"my-project\")\n```\n\nThat's it. No routers, no argument parsers, no tool definitions.\n\n---\n\n## What you get\n\n| Interface | How to get it | What it gives you |\n|-----------|--------------|-------------------|\n| **REST API** | `app.api()` | FastAPI app with auto-generated endpoints |\n| **CLI** | `app.cli()` | Click group with `serve`, `list`, and one command per function |\n| **MCP tools** | `app.mcp()` | FastAPI + MCP server for AI agents / LLMs |\n| **Frontend SDK** | `import { RefractClient } from '/refract/client.js'` | Typed JS client for browsers and frameworks |\n\n---\n\n## 🚀 Quick start\n\n**Install:**\n\n```bash\npip install git+https://github.com/Brunvelop/refract\n```\n\n**Try the demo right now:**\n\n```bash\ngit clone https://github.com/Brunvelop/refract \u0026\u0026 cd refract\npython demo.py serve          # API + MCP + Web UI at http://localhost:8000\npython demo.py list           # List all registered functions\npython demo.py add --a 3 --b 5\npython demo.py greet --name World\n```\n\n---\n\n## 🧩 Define functions\n\nEvery function follows the same pattern: **typed signature + docstring + `BaseModel` return type**.\n\n```python\nfrom pydantic import BaseModel\nfrom refract import register_function\n\nclass SearchResponse(BaseModel):\n    items: list[str]\n    total: int\n\n@register_function(http_methods=[\"GET\"])\ndef search(query: str, limit: int = 10) -\u003e SearchResponse:\n    \"\"\"Search items.\n    Args:\n        query: Search term\n        limit: Maximum number of results\n    \"\"\"\n    results = [\"foo\", \"bar\", \"baz\"][:limit]\n    return SearchResponse(items=results, total=len(results))\n```\n\nThe return type becomes the FastAPI `response_model` — precise OpenAPI schema, full type safety, and the exact same shape on every interface.\n\n### Decorator options\n\n```python\n@register_function(\n    http_methods=[\"GET\", \"POST\"],   # default: [\"GET\", \"POST\"]\n    interfaces=[\"api\", \"cli\"],      # default: [\"api\", \"cli\", \"mcp\"]\n    streaming=False,                # default: False\n    stream_func=None,               # required if streaming=True\n)\n```\n\n\u003e **Note:** `async def` functions are fully supported on **API** and **MCP** interfaces — they will be properly awaited. The **CLI** interface only supports synchronous functions; async functions are automatically skipped with a warning at CLI build time.\n\n\u003e **CLI limitations:** Parameters with complex types (`list`, `dict`, `List[str]`, `Dict[str, int]`, etc.) fall back to plain strings in the CLI interface. Click only natively supports `int`, `float`, `bool`, and `str`. For functions that require structured inputs, consider restricting them to `interfaces=[\"api\", \"mcp\"]`, or accept a JSON string and parse it inside the function.\n\n---\n\n## 📐 Setup modes\n\nThe setup scales progressively. Each mode adds one or two lines — never a new file.\n\n### Mode 1 — One line (recommended default)\n\n```python\n# my_project/app.py\nfrom refract import Refract\n\napp = Refract(\"my-project\", discover=[\"my_project.core\"])\n```\n\nWire it up as a CLI entry point:\n\n```toml\n# pyproject.toml\n[project.scripts]\nmy-project = \"my_project.app:app.run_cli\"\n```\n\nYou immediately get:\n\n```bash\nmy-project serve          # Start unified server (API + MCP + UI) at http://0.0.0.0:8000\nmy-project serve-api      # Start REST API only at http://127.0.0.1:8000\nmy-project serve-mcp      # Start MCP-only server at http://127.0.0.1:8001\nmy-project list           # List all registered functions\nmy-project add --a 1 --b 2   # Call any registered function directly\nmy-project --verbose serve   # Enable DEBUG logging\n```\n\n\u003e **No boilerplate.** The `discover=` list tells Refract which packages to scan for `@register_function` decorators. Everything else is automatic.\n\n\u003e **Custom views and static assets** — Pass `views` to replace the default dashboard with your own HTML pages, and `static_dirs` to serve additional static directories:\n\u003e\n\u003e ```python\n\u003e app = Refract(\"my-project\",\n\u003e     discover=[\"my_project.core\"],\n\u003e     views={\"/\": \"templates/index.html\", \"/about\": \"templates/about.html\"},\n\u003e     static_dirs=[(\"/static\", \"my_app/static\")],\n\u003e )\n\u003e ```\n\u003e\n\u003e The `/refract/` namespace is reserved for the SDK JS files and must not be used in `static_dirs`.\n\n\u003e **Advanced uvicorn options** — The built-in `serve` commands are convenience wrappers that support `--host` and `--port`. Features like auto-reload, multiple workers, or custom log levels require uvicorn to be invoked with a **string import path** pointing to an ASGI application. `app.run_cli` is a Click group, not an ASGI app — expose `app.api()` as a module-level variable instead:\n\u003e\n\u003e ```python\n\u003e # my_project/app.py\n\u003e from refract import Refract\n\u003e\n\u003e app = Refract(\"my-project\", discover=[\"my_project.core\"])\n\u003e fastapi_app = app.api()  # ASGI app — this is what uvicorn needs\n\u003e ```\n\u003e\n\u003e ```bash\n\u003e # Auto-reload in development\n\u003e uvicorn my_project.app:fastapi_app --reload\n\u003e\n\u003e # Multiple workers for production\n\u003e uvicorn my_project.app:fastapi_app --workers 4 --host 0.0.0.0 --port 8000\n\u003e\n\u003e # Custom log level\n\u003e uvicorn my_project.app:fastapi_app --log-level warning\n\u003e ```\n\u003e\n\u003e See the [uvicorn docs](https://www.uvicorn.org/settings/) for the full list of available options.\n\n### Mode 2 — Custom CLI commands\n\nSame file, add `@app.command()`:\n\n```python\n# my_project/app.py\nfrom refract import Refract\n\napp = Refract(\"my-project\", discover=[\"my_project.core\"])\n\n@app.command()\ndef health_check():\n    \"\"\"Run project health checks.\"\"\"\n    import subprocess\n    subprocess.run([\"pytest\", \"tests/health/\", \"-q\"])\n```\n\n```bash\nmy-project health-check   # Your custom command, alongside serve/list\n```\n\nCustom commands use Click under the hood — `click.echo`, `click.option`, etc. work normally.\n\n### Mode 3 — Bring your own FastAPI app\n\nUse `app.router()` to mount only the function endpoints onto your own FastAPI instance:\n\n```python\n# my_project/app.py\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom refract import Refract\n\nrefract_app = Refract(\"my-project\", discover=[\"my_project.core\"])\n\nmy_app = FastAPI(title=\"My Project\", version=\"1.0.0\")\nmy_app.add_middleware(CORSMiddleware, allow_origins=[\"*\"])\nmy_app.include_router(refract_app.router())\n\n@my_app.get(\"/status\")\ndef status():\n    return {\"ok\": True}\n```\n\n`app.router()` includes:\n- `POST/GET /{funcName}` — one endpoint per registered function\n- `GET /functions/details` — JSON Schema discovery for the frontend\n- `GET /health` — basic health check\n\n### MCP sidecar — `mcp_only()`\n\nFor dedicated AI agent deployments, use `mcp_only()` to spin up a minimal FastAPI app with **only** MCP tool endpoints and a `/health` check — no REST API, no HTML pages, no static files:\n\n```python\n# Sidecar deployment: MCP tools without a full REST API\nmcp_app = app.mcp_only()\n```\n\nThis is also what `serve-mcp` uses under the hood, making it ideal for a separate MCP process alongside an existing REST API server.\n\n### Accessing the current instance — `Refract.current()`\n\nFollowing the Flask `current_app` pattern, `Refract.current()` returns the most recently created instance. Useful in business-logic modules that must not import the app module directly (to avoid circular imports):\n\n```python\nfrom refract import Refract\n\n# In a module that must not import app.py:\ninstance = Refract.current()\nmcp_functions = instance.get_functions_for_interface(\"mcp\")\n```\n\n---\n\n## 📡 Streaming (SSE)\n\nFor long-running functions (LLM calls, background jobs), use Server-Sent Events:\n\n```python\nimport json\nimport asyncio\nfrom pydantic import BaseModel\nfrom refract import register_function\nfrom refract.sse import format_sse\n\nclass ProcessResult(BaseModel):\n    text: str\n\nasync def _stream_process(text: str):\n    \"\"\"Async generator that yields SSE-formatted events.\"\"\"\n    words = text.split()\n    for word in words:\n        await asyncio.sleep(0.1)\n        yield format_sse(\"token\", json.dumps({\"chunk\": word}))\n    yield format_sse(\"complete\", json.dumps({\"message\": \"done\"}))\n\n@register_function(streaming=True, stream_func=_stream_process)\ndef process_text(text: str) -\u003e ProcessResult:\n    \"\"\"Process text word by word.\n    Args:\n        text: Text to process\n    \"\"\"\n    return ProcessResult(text=text)\n```\n\nOn the frontend, consume it with `RefractClient.stream()`:\n\n```javascript\nimport { RefractClient } from '/refract/client.js';\nconst api = new RefractClient();\n\nfor await (const { event, data } of api.stream('process_text', { text: 'hello world' })) {\n    if (event === 'token') console.log(data.chunk);\n    if (event === 'complete') console.log('Done:', data.message);\n}\n```\n\n---\n\n## 🌐 Frontend\n\n### `RefractClient` (vanilla JS, no framework)\n\nPure HTTP client for calling Refract functions from JavaScript.\nWorks in any context — vanilla JS, React, Vue, Lit, tests.\n\n```javascript\nimport { RefractClient } from '/refract/client.js';\n\nconst api = new RefractClient();\n\n// Call a function — schemas are auto-loaded and cached on the first call\nconst data = await api.call('add', { a: 1, b: 2 });\n// → { result: 3 }  — your Pydantic model, as returned by the API\n\n// Stream (SSE)\nfor await (const { event, data } of api.stream('process', { text: 'hello' })) {\n    if (event === 'token')    console.log(data.chunk);\n    if (event === 'complete') console.log('Done');\n}\n\n// Access schemas (type info from Python)\nawait api.loadSchemas();\nconst schema = api.getSchema('add');\n// schema.parameters    → [{ name: 'a', type: 'int', required: true }, ...]\n// schema.response_schema → { properties: { result: { type: 'integer' } } }\n\n// Validate before calling (for form UX)\nconst { valid, errors } = api.validate('add', { a: 1 });\n// → { valid: false, errors: { b: 'Required' } }\n```\n\nParameters are validated against Python type definitions before each call (opt-in via `{ validate: true }`).\nNo type coercion — pass the correct JS types matching your Python signatures.\nThe response is returned as-is: it is the serialised form of your Pydantic model.\n\n---\n\n## 🖥 Dashboard\n\n`app.api()` (and `serve`) include a built-in dashboard at `/` and `/functions`. No configuration needed:\n\n- **Health badge** — live status from `GET /health` (function count + healthy/unreachable)\n- **Quick links** — one-click access to Swagger UI, ReDoc, MCP endpoint, Health check, and Schema JSON\n- **Registry table** — all registered functions with name, description, HTTP methods, and interface badges (API / CLI / MCP / SSE)\n- **MCP panel** — the full endpoint URL and connection string (with copy buttons), automatically hidden when MCP is not mounted\n\nFor interactive testing of your functions, use **Swagger UI** (`/docs`) — it's purpose-built for that.\n\n---\n\n## 🔗 Schema sharing (back ↔ front)\n\nEvery function schema includes a `response_schema` — the JSON Schema generated by Pydantic from the return type. This is the contract between backend and frontend.\n\n```\nGET /functions/details\n```\n\n```json\n{\n  \"functions\": {\n    \"add\": {\n      \"name\": \"add\",\n      \"description\": \"Add two numbers.\",\n      \"http_methods\": [\"GET\", \"POST\"],\n      \"parameters\": [\n        { \"name\": \"a\", \"type\": \"int\", \"required\": true, \"description\": \"First number\" },\n        { \"name\": \"b\", \"type\": \"int\", \"required\": true, \"description\": \"Second number\" }\n      ],\n      \"streaming\": false,\n      \"response_schema\": {\n        \"properties\": {\n          \"result\": { \"title\": \"Result\", \"type\": \"integer\" }\n        },\n        \"required\": [\"result\"],\n        \"title\": \"Sum\",\n        \"type\": \"object\"\n      }\n    }\n  }\n}\n```\n\nThe frontend always knows the exact shape of the response — enabling runtime validation, type-safe consumers, and future codegen.\n\n---\n\n## 🔍 Discovery logging\n\nDiscovery is **lazy** — modules are imported on the first registry query, not at instantiation time. When Refract scans your packages, you see exactly what happened:\n\n```\n[refract:my-project] Scanning my_project.core...\n[refract:my-project]   ✅ my_project.core.math — 2 functions\n[refract:my-project]   ✅ my_project.core.search — 1 function\n[refract:my-project]   ⚠️  my_project.core.ai — skipped (ImportError: dspy not installed)\n[refract:my-project]   ℹ️  my_project.core.models — no @register_function found\n[refract:my-project] Total: 3 functions registered, 1 module skipped\n```\n\nDiscovery is resilient by default — import errors are logged and skipped. Use `--verbose` to enable DEBUG-level output.\n\n---\n\n## 🏗 Architecture\n\n```\nrefract/\n├── refract/\n│   ├── __init__.py       # Public API: Refract, register_function (version falls back to `0.0.0-dev` when not installed)\n│   ├── models.py         # ParamSchema, FunctionInfo, FunctionSchema\n│   ├── registry.py       # @register_function decorator + Registry class\n│   ├── refract.py        # Refract facade class\n│   ├── api.py            # FastAPI app/router factories\n│   ├── cli.py            # Click group factory\n│   ├── mcp.py            # FastAPI + MCP factory\n│   ├── sse.py            # format_sse(), _create_stream_handler()\n│   ├── log_config.py     # configure_cli_logging(), configure_api_logging()\n│   └── web/\n│       ├── client.js     # RefractClient — typed JS client (vanilla JS, no framework)\n│       └── views/\n│           └── dashboard.html  # Default web UI (served at / and /functions)\n├── demo.py               # Runnable demo — try it right now\n└── tests/\n    ├── conftest.py\n    ├── test_models.py\n    ├── test_registry.py\n    ├── test_api.py\n    ├── test_cli.py\n    ├── test_mcp.py\n    └── test_sse.py\n```\n\n---\n\n## 📖 API Reference\n\n### `Refract(name, discover=None, views=None, static_dirs=None)`\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `name` | `str` | — | Human-readable name for this instance (used in logs and API title) |\n| `discover` | `list[str] \\| None` | `None` | Package paths to scan for `@register_function` decorators |\n| `views` | `dict[str, str] \\| None` | `None` | URL path → HTML file path mapping; replaces the default dashboard |\n| `static_dirs` | `list[tuple[str, str]] \\| None` | `None` | Extra `(mount_path, directory)` pairs for static file serving |\n\n| Method / Property | Returns | Description |\n|---|---|---|\n| `.api()` | `FastAPI` | Complete FastAPI app with static files and HTML views |\n| `.router()` | `APIRouter` | Only the function endpoints (mount in your own app) |\n| `.cli()` | `click.Group` | Click group with built-in and function commands |\n| `.mcp()` | `FastAPI` | FastAPI app with full API + MCP integration |\n| `.mcp_only()` | `FastAPI` | MCP-only FastAPI app (no REST API, no UI, no static files) |\n| `.run_cli` | `click.Group` | Cached property — use as `pyproject.toml` entry point |\n| `@.command(name=None, **kwargs)` | decorator | Register a custom Click command; `name` defaults to the function name with `_` → `-` |\n| `.current()` | `Registry` | Classmethod — returns the most recently created instance (Flask `current_app` pattern) |\n| `.get_all_functions()` | `list[FunctionInfo]` | All registered functions |\n| `.get_all_schemas()` | `list[FunctionSchema]` | Serialisable schemas (JSON-safe) |\n| `.get_function_by_name(name)` | `FunctionInfo \\| None` | Look up a function by name |\n| `.function_count()` | `int` | Number of registered functions |\n\n### `register_function(...)`\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `http_methods` | `list[str]` | `[\"GET\", \"POST\"]` | HTTP verbs to expose on the API |\n| `interfaces` | `list[str]` | `[\"api\", \"cli\", \"mcp\"]` | Which interfaces to enable |\n| `streaming` | `bool` | `False` | Enable SSE streaming mode |\n| `stream_func` | `Callable \\| None` | `None` | Async generator for streaming (required if `streaming=True`) |\n\n### `format_sse(event, data)`\n\n```python\nfrom refract.sse import format_sse\n\nline = format_sse(\"token\", '{\"chunk\": \"hello\"}')\n# → \"event: token\\ndata: {\\\"chunk\\\": \\\"hello\\\"}\\n\\n\"\n```\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrunvelop%2Frefract","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrunvelop%2Frefract","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrunvelop%2Frefract/lists"}