# Refract

> One Python function → REST API, CLI, MCP tools for AI agents & Web UI. Zero boilerplate.

- **Type:** MCP server
- **Install:** `agentstack add mcp-brunvelop-refract`
- **Verified:** Pending review
- **Seller:** [Brunvelop](https://agentstack.voostack.com/s/brunvelop)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Brunvelop](https://github.com/Brunvelop)
- **Source:** https://github.com/Brunvelop/refract

## Install

```sh
agentstack add mcp-brunvelop-refract
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# refract 💎

> **One Python function. Four interfaces. Zero boilerplate.**

[](https://www.python.org/downloads/)
[](LICENSE)
[]()

Define a typed Python function once — and automatically get a **REST API**, a **CLI**, **MCP tools** for AI agents, and a **Frontend SDK**.

```python
from pydantic import BaseModel
from refract import Refract, register_function

class Sum(BaseModel):
    result: int

@register_function()
def add(a: int, b: int) -> Sum:
    """Add two numbers.
    Args:
        a: First number
        b: Second number
    """
    return Sum(result=a + b)

app = Refract("my-project")
```

That's it. No routers, no argument parsers, no tool definitions.

---

## What you get

| Interface | How to get it | What it gives you |
|-----------|--------------|-------------------|
| **REST API** | `app.api()` | FastAPI app with auto-generated endpoints |
| **CLI** | `app.cli()` | Click group with `serve`, `list`, and one command per function |
| **MCP tools** | `app.mcp()` | FastAPI + MCP server for AI agents / LLMs |
| **Frontend SDK** | `import { RefractClient } from '/refract/client.js'` | Typed JS client for browsers and frameworks |

---

## 🚀 Quick start

**Install:**

```bash
pip install git+https://github.com/Brunvelop/refract
```

**Try the demo right now:**

```bash
git clone https://github.com/Brunvelop/refract && cd refract
python demo.py serve          # API + MCP + Web UI at http://localhost:8000
python demo.py list           # List all registered functions
python demo.py add --a 3 --b 5
python demo.py greet --name World
```

---

## 🧩 Define functions

Every function follows the same pattern: **typed signature + docstring + `BaseModel` return type**.

```python
from pydantic import BaseModel
from refract import register_function

class SearchResponse(BaseModel):
    items: list[str]
    total: int

@register_function(http_methods=["GET"])
def search(query: str, limit: int = 10) -> SearchResponse:
    """Search items.
    Args:
        query: Search term
        limit: Maximum number of results
    """
    results = ["foo", "bar", "baz"][:limit]
    return SearchResponse(items=results, total=len(results))
```

The return type becomes the FastAPI `response_model` — precise OpenAPI schema, full type safety, and the exact same shape on every interface.

### Decorator options

```python
@register_function(
    http_methods=["GET", "POST"],   # default: ["GET", "POST"]
    interfaces=["api", "cli"],      # default: ["api", "cli", "mcp"]
    streaming=False,                # default: False
    stream_func=None,               # required if streaming=True
)
```

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

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

---

## 📐 Setup modes

The setup scales progressively. Each mode adds one or two lines — never a new file.

### Mode 1 — One line (recommended default)

```python
# my_project/app.py
from refract import Refract

app = Refract("my-project", discover=["my_project.core"])
```

Wire it up as a CLI entry point:

```toml
# pyproject.toml
[project.scripts]
my-project = "my_project.app:app.run_cli"
```

You immediately get:

```bash
my-project serve          # Start unified server (API + MCP + UI) at http://0.0.0.0:8000
my-project serve-api      # Start REST API only at http://127.0.0.1:8000
my-project serve-mcp      # Start MCP-only server at http://127.0.0.1:8001
my-project list           # List all registered functions
my-project add --a 1 --b 2   # Call any registered function directly
my-project --verbose serve   # Enable DEBUG logging
```

> **No boilerplate.** The `discover=` list tells Refract which packages to scan for `@register_function` decorators. Everything else is automatic.

> **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:
>
> ```python
> app = Refract("my-project",
>     discover=["my_project.core"],
>     views={"/": "templates/index.html", "/about": "templates/about.html"},
>     static_dirs=[("/static", "my_app/static")],
> )
> ```
>
> The `/refract/` namespace is reserved for the SDK JS files and must not be used in `static_dirs`.

> **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:
>
> ```python
> # my_project/app.py
> from refract import Refract
>
> app = Refract("my-project", discover=["my_project.core"])
> fastapi_app = app.api()  # ASGI app — this is what uvicorn needs
> ```
>
> ```bash
> # Auto-reload in development
> uvicorn my_project.app:fastapi_app --reload
>
> # Multiple workers for production
> uvicorn my_project.app:fastapi_app --workers 4 --host 0.0.0.0 --port 8000
>
> # Custom log level
> uvicorn my_project.app:fastapi_app --log-level warning
> ```
>
> See the [uvicorn docs](https://www.uvicorn.org/settings/) for the full list of available options.

### Mode 2 — Custom CLI commands

Same file, add `@app.command()`:

```python
# my_project/app.py
from refract import Refract

app = Refract("my-project", discover=["my_project.core"])

@app.command()
def health_check():
    """Run project health checks."""
    import subprocess
    subprocess.run(["pytest", "tests/health/", "-q"])
```

```bash
my-project health-check   # Your custom command, alongside serve/list
```

Custom commands use Click under the hood — `click.echo`, `click.option`, etc. work normally.

### Mode 3 — Bring your own FastAPI app

Use `app.router()` to mount only the function endpoints onto your own FastAPI instance:

```python
# my_project/app.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from refract import Refract

refract_app = Refract("my-project", discover=["my_project.core"])

my_app = FastAPI(title="My Project", version="1.0.0")
my_app.add_middleware(CORSMiddleware, allow_origins=["*"])
my_app.include_router(refract_app.router())

@my_app.get("/status")
def status():
    return {"ok": True}
```

`app.router()` includes:
- `POST/GET /{funcName}` — one endpoint per registered function
- `GET /functions/details` — JSON Schema discovery for the frontend
- `GET /health` — basic health check

### MCP sidecar — `mcp_only()`

For 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:

```python
# Sidecar deployment: MCP tools without a full REST API
mcp_app = app.mcp_only()
```

This is also what `serve-mcp` uses under the hood, making it ideal for a separate MCP process alongside an existing REST API server.

### Accessing the current instance — `Refract.current()`

Following 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):

```python
from refract import Refract

# In a module that must not import app.py:
instance = Refract.current()
mcp_functions = instance.get_functions_for_interface("mcp")
```

---

## 📡 Streaming (SSE)

For long-running functions (LLM calls, background jobs), use Server-Sent Events:

```python
import json
import asyncio
from pydantic import BaseModel
from refract import register_function
from refract.sse import format_sse

class ProcessResult(BaseModel):
    text: str

async def _stream_process(text: str):
    """Async generator that yields SSE-formatted events."""
    words = text.split()
    for word in words:
        await asyncio.sleep(0.1)
        yield format_sse("token", json.dumps({"chunk": word}))
    yield format_sse("complete", json.dumps({"message": "done"}))

@register_function(streaming=True, stream_func=_stream_process)
def process_text(text: str) -> ProcessResult:
    """Process text word by word.
    Args:
        text: Text to process
    """
    return ProcessResult(text=text)
```

On the frontend, consume it with `RefractClient.stream()`:

```javascript
import { RefractClient } from '/refract/client.js';
const api = new RefractClient();

for await (const { event, data } of api.stream('process_text', { text: 'hello world' })) {
    if (event === 'token') console.log(data.chunk);
    if (event === 'complete') console.log('Done:', data.message);
}
```

---

## 🌐 Frontend

### `RefractClient` (vanilla JS, no framework)

Pure HTTP client for calling Refract functions from JavaScript.
Works in any context — vanilla JS, React, Vue, Lit, tests.

```javascript
import { RefractClient } from '/refract/client.js';

const api = new RefractClient();

// Call a function — schemas are auto-loaded and cached on the first call
const data = await api.call('add', { a: 1, b: 2 });
// → { result: 3 }  — your Pydantic model, as returned by the API

// Stream (SSE)
for await (const { event, data } of api.stream('process', { text: 'hello' })) {
    if (event === 'token')    console.log(data.chunk);
    if (event === 'complete') console.log('Done');
}

// Access schemas (type info from Python)
await api.loadSchemas();
const schema = api.getSchema('add');
// schema.parameters    → [{ name: 'a', type: 'int', required: true }, ...]
// schema.response_schema → { properties: { result: { type: 'integer' } } }

// Validate before calling (for form UX)
const { valid, errors } = api.validate('add', { a: 1 });
// → { valid: false, errors: { b: 'Required' } }
```

Parameters are validated against Python type definitions before each call (opt-in via `{ validate: true }`).
No type coercion — pass the correct JS types matching your Python signatures.
The response is returned as-is: it is the serialised form of your Pydantic model.

---

## 🖥 Dashboard

`app.api()` (and `serve`) include a built-in dashboard at `/` and `/functions`. No configuration needed:

- **Health badge** — live status from `GET /health` (function count + healthy/unreachable)
- **Quick links** — one-click access to Swagger UI, ReDoc, MCP endpoint, Health check, and Schema JSON
- **Registry table** — all registered functions with name, description, HTTP methods, and interface badges (API / CLI / MCP / SSE)
- **MCP panel** — the full endpoint URL and connection string (with copy buttons), automatically hidden when MCP is not mounted

For interactive testing of your functions, use **Swagger UI** (`/docs`) — it's purpose-built for that.

---

## 🔗 Schema sharing (back ↔ front)

Every function schema includes a `response_schema` — the JSON Schema generated by Pydantic from the return type. This is the contract between backend and frontend.

```
GET /functions/details
```

```json
{
  "functions": {
    "add": {
      "name": "add",
      "description": "Add two numbers.",
      "http_methods": ["GET", "POST"],
      "parameters": [
        { "name": "a", "type": "int", "required": true, "description": "First number" },
        { "name": "b", "type": "int", "required": true, "description": "Second number" }
      ],
      "streaming": false,
      "response_schema": {
        "properties": {
          "result": { "title": "Result", "type": "integer" }
        },
        "required": ["result"],
        "title": "Sum",
        "type": "object"
      }
    }
  }
}
```

The frontend always knows the exact shape of the response — enabling runtime validation, type-safe consumers, and future codegen.

---

## 🔍 Discovery logging

Discovery is **lazy** — modules are imported on the first registry query, not at instantiation time. When Refract scans your packages, you see exactly what happened:

```
[refract:my-project] Scanning my_project.core...
[refract:my-project]   ✅ my_project.core.math — 2 functions
[refract:my-project]   ✅ my_project.core.search — 1 function
[refract:my-project]   ⚠️  my_project.core.ai — skipped (ImportError: dspy not installed)
[refract:my-project]   ℹ️  my_project.core.models — no @register_function found
[refract:my-project] Total: 3 functions registered, 1 module skipped
```

Discovery is resilient by default — import errors are logged and skipped. Use `--verbose` to enable DEBUG-level output.

---

## 🏗 Architecture

```
refract/
├── refract/
│   ├── __init__.py       # Public API: Refract, register_function (version falls back to `0.0.0-dev` when not installed)
│   ├── models.py         # ParamSchema, FunctionInfo, FunctionSchema
│   ├── registry.py       # @register_function decorator + Registry class
│   ├── refract.py        # Refract facade class
│   ├── api.py            # FastAPI app/router factories
│   ├── cli.py            # Click group factory
│   ├── mcp.py            # FastAPI + MCP factory
│   ├── sse.py            # format_sse(), _create_stream_handler()
│   ├── log_config.py     # configure_cli_logging(), configure_api_logging()
│   └── web/
│       ├── client.js     # RefractClient — typed JS client (vanilla JS, no framework)
│       └── views/
│           └── dashboard.html  # Default web UI (served at / and /functions)
├── demo.py               # Runnable demo — try it right now
└── tests/
    ├── conftest.py
    ├── test_models.py
    ├── test_registry.py
    ├── test_api.py
    ├── test_cli.py
    ├── test_mcp.py
    └── test_sse.py
```

---

## 📖 API Reference

### `Refract(name, discover=None, views=None, static_dirs=None)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `name` | `str` | — | Human-readable name for this instance (used in logs and API title) |
| `discover` | `list[str] \| None` | `None` | Package paths to scan for `@register_function` decorators |
| `views` | `dict[str, str] \| None` | `None` | URL path → HTML file path mapping; replaces the default dashboard |
| `static_dirs` | `list[tuple[str, str]] \| None` | `None` | Extra `(mount_path, directory)` pairs for static file serving |

| Method / Property | Returns | Description |
|---|---|---|
| `.api()` | `FastAPI` | Complete FastAPI app with static files and HTML views |
| `.router()` | `APIRouter` | Only the function endpoints (mount in your own app) |
| `.cli()` | `click.Group` | Click group with built-in and function commands |
| `.mcp()` | `FastAPI` | FastAPI app with full API + MCP integration |
| `.mcp_only()` | `FastAPI` | MCP-only FastAPI app (no REST API, no UI, no static files) |
| `.run_cli` | `click.Group` | Cached property — use as `pyproject.toml` entry point |
| `@.command(name=None, **kwargs)` | decorator | Register a custom Click command; `name` defaults to the function name with `_` → `-` |
| `.current()` | `Registry` | Classmethod — returns the most recently created instance (Flask `current_app` pattern) |
| `.get_all_functions()` | `list[FunctionInfo]` | All registered functions |
| `.get_all_schemas()` | `list[FunctionSchema]` | Serialisable schemas (JSON-safe) |
| `.get_function_by_name(name)` | `FunctionInfo \| None` | Look up a function by name |
| `.function_count()` | `int` | Number of registered functions |

### `register_function(...)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `http_methods` | `list[str]` | `["GET", "POST"]` | HTTP verbs to expose on the API |
| `interfaces` | `list[str]` | `["api", "cli", "mcp"]` | Which interfaces to enable |
| `streaming` | `bool` | `False` | Enable S

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Brunvelop](https://github.com/Brunvelop)
- **Source:** [Brunvelop/refract](https://github.com/Brunvelop/refract)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-brunvelop-refract
- Seller: https://agentstack.voostack.com/s/brunvelop
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
