JSEE for Python

Serve a Python function as a browser form and HTTP API. JSEE generates the form from type hints and runs the function in Python. The base server uses only the standard library; MCP support and model libraries are optional dependencies.

This guide covers Python installation, function annotations and server setup. The main README defines the shared schema, HTTP response formats and MCP protocol; the sections here show how to use them from Python.

Main JSEE documentation Quick start Type mapping Python API CLI MCP

Install

python -m pip install jsee==0.9.0

Requires Python 3.9+; MCP requires Python 3.10+ and jsee[server].

The wheel includes the browser runtime, so installing or serving it requires no Node.js. Individual models may need additional Python packages.

Quick start

Save this complete program as app.py:

import jsee


def multiply(a: float, b: float = 2.0) -> float:
    return a * b


if __name__ == '__main__':
    jsee.serve(multiply, port=5050)

Run python app.py and open http://localhost:5050. Enter 3 and 4, then click Run to get 12. Or call the same function from another terminal:

curl http://localhost:5050/multiply -H 'Content-Type: application/json' -d '{"a":3,"b":4}'

The response is {"result":12.0}. Stop with Ctrl+C. You can also serve the function with jsee app.py multiply --port 5050; the main guard keeps importing the file from starting a second server.

This callable-based server provides the UI at /, the function at POST /multiply, discovery at /api and OpenAPI at /api/openapi.json. For a schema-backed app with the shared /run route and MCP, see Use a schema file.

How it works

jsee.serve(multiply) reads the function’s parameters, type hints and defaults to create a JSEE schema. The browser uses that schema to display controls and sends their values to POST /multiply. Python calls the function; the browser displays the result. The function runs on the server, not in the browser.

You can also write the schema yourself to specify file inputs, nested JSON, tables, multiple outputs or a pipeline. The two serving modes differ:

Start with Execution and response
jsee.serve(function) or jsee app.py multiply One function at /multiply; simple results such as {"result": 12}
jsee serve schema.json Whole pipeline at /run; declared outputs and stored file references

Both serve a browser UI at /. Use a schema for JSON CLI runs and MCP tools. The main README describes the shared schema and browser API.

Type mapping

These are input hints understood by generate_schema() and callable-based serving. They select controls and validation rules; they do not convert JSON values into arbitrary Python objects.

Python type JSEE type Control
int int Number field
float float Number field
bool checkbox Checkbox
str (or no hint) string Text field
datetime.date date Date picker
Literal["a", "b"] select Dropdown
Enum subclass select Dropdown
Optional[X] Type for X Same as X

Optional[X] adds nullable: true; Python 3.10+ also supports X | None. Dates arrive as strings such as "2026-10-11"; Enum choices arrive as member values, not Enum instances. Convert them inside your function if needed. A None default is allowed; other defaults must match the declared input type.

Plain list, dict, bytes and unrecognized input hints currently fall back to string. To accept structured data or files, write explicit object, table, file or media inputs in a schema. JSEE does not infer their structure from Python collection hints.

Annotated types

Use typing.Annotated to choose a control and its options:

from typing import Annotated
import jsee

def predict(
    text: Annotated[str, jsee.Text()],
    temperature: Annotated[float, jsee.Slider(0, 2, 0.1)] = 0.7,
    mode: Annotated[str, jsee.Radio(['greedy', 'sample'])] = 'sample'
) -> str:
    return text.upper()

jsee.serve(predict)
Annotation JSEE type GUI widget
jsee.Slider(min, max, step) slider Range slider
jsee.Text() text Textarea
jsee.Radio(options) radio Radio buttons
jsee.Select(options) select Dropdown
jsee.MultiSelect(options) multi-select Checkbox group
jsee.Range(min, max, step) range Dual-handle slider; returns a two-number list
jsee.Color() color Color picker

Use a list hint with MultiSelect and Range: both controls return lists.

Output types

Use typing.Annotated with output descriptors on the return type, or declare outputs via the outputs kwarg:

from typing import Annotated
import jsee

# Option 1: return type annotation
def summarize(text: str) -> Annotated[str, jsee.Markdown()]:
    return '## Summary\n\n' + text[:100] + '...'

# Option 2: outputs kwarg (for multi-output dicts)
def report(query: str) -> dict:
    rows = [{'word': w, 'len': len(w)} for w in query.split()]
    return {'summary': '**Found {}**'.format(len(rows)), 'data': rows}

jsee.serve(report, outputs={'summary': jsee.Markdown(), 'data': jsee.Table()})
Annotation JSEE output type Renders as
jsee.Markdown() markdown Formatted Markdown
jsee.Html() html HTML; sanitization follows security.allowRawHtml
jsee.Code() code Code block (<pre>)
jsee.Image() image <img> tag
jsee.Table() table Sortable table
jsee.Svg() svg Inline SVG
jsee.File(filename) file Download button

Output dictionaries/lists also accept the shared schema types without annotation helpers. For ranked scores, use outputs=[{'name': 'predictions', 'type': 'scores', 'format': 'percent'}] and return {'predictions': [{'label': 'cat', 'score': 0.72}]}. Canonical CLI/HTTP/MCP preserve row order and numeric values; the browser sorts the display. Scores must be finite. See the shared output reference.

A list return hint generates a table output. Other plain return hints leave output selection to browser auto-detection: dictionaries produce outputs by key, and returned bytes become MIME-sniffed data URLs. See return values for serialization. The JSON CLI, /run and MCP require declared outputs.

Use a schema file

The browser, CLI, HTTP and MCP tutorial supplies a complete descriptor and input file. Keep its schema.json and input.json, and save this as model.py alongside them:

def multiply(a: float, b: float) -> dict:
    product = a * b
    return {'product': product, 'report': f'{a:g} × {b:g} = {product:g}\n'}

In schema.json, replace only model with:

{"url":"model.py","type":"py","entry":"multiply"}

Keep the same inputs, outputs and input.json. Use the Python environment’s jsee executable:

jsee inspect schema.json --json
jsee validate schema.json
jsee run schema.json --input input.json --json
jsee serve schema.json --port 3000

The product and report match the JavaScript version. These commands and the served form need no Node.js. To call the same descriptor from an agent, follow the MCP setup.

This step uses native Python on the server. Browser-only Python uses Pyodide and needs a browser-compatible package set; it is a separate deployment choice.

Model loading

For schema-backed apps, model.url points to a local .py file. Paths resolve from the descriptor’s directory, or the current directory for a schema dict.

  • entry selects the callable; name separately names its HTTP endpoint.
  • Without entry, JSEE looks up name. If both are omitted, it selects a public callable defined in that file and serves it at /model.
  • Endpoint names must be unique. Stages in one application share a module instance when they use the same file; separate applications load separate instances.
  • Native Python supports ordinary sequential pipelines. Named-stage bindings, schema imports, remote models and canonical streaming are unsupported. Imports inside Python source work normally.

The CSV summary example uses one descriptor and function for native Python and browser Pyodide.

Descriptor validation

The package also includes the descriptor authoring JSON Schema (draft-07), accessible without loading model code:

import json
from importlib.resources import files

descriptor_schema = json.loads(files('jsee').joinpath('static/schema.json').read_text())

Use it with a JSON Schema validator of your choice. It describes known fields while allowing extension metadata; runtime capability and invocation-value checks remain separate.

JSON commands and canonical HTTP

Use these commands with a descriptor declaring local Python models, inputs and outputs. “Canonical HTTP” refers to the whole-pipeline /run endpoint.

jsee inspect schema.json --json
jsee validate schema.json
jsee run schema.json --input input.json --json
jsee serve schema.json --port 5050

inspect and validate do not import models. run reads JSON from stdin when --input is omitted or -; model prints go to stderr.

Result JSON response CLI exit code
Success {runId, status: "succeeded", outputs, artifacts} 0
Invalid descriptor or request Failed status with error: {code, message} 2
Invocation or output failure Failed status with error: {code, message} 1

Programmatically, use jsee.serve(schema, canonical=True) or jsee.create_app(schema, canonical=True). Here schema is a file path or a schema dict referencing local model files, not a function. A stage named run conflicts with /run; give it another name. Omitting canonical=True keeps the per-model routes and does not create /run or /schema. The CLI subcommand jsee serve schema.json enables canonical serving automatically.

Input and output validation

Inputs are checked before execution: types, options, numeric bounds/steps and required fields. Defaults apply without mutating the submitted dict. Named groups become nested objects; unnamed groups flatten into their parent.

object accepts JSON, optionally restricted by valueSchema; table accepts row dictionaries or {columns, rows}. See the shared input reference for shapes and media restrictions.

Canonical results contain only declared outputs, resolving aliases and flattening output groups. Before storing files, JSEE checks their declared types and shapes. Missing required outputs, unsupported objects and non-finite numbers fail with JSEE_OUTPUT_INVALID; errors identify the path, such as info.a: NaN is not a JSON value. Numeric outputs reject None.

Object valueSchema supports a bounded vocabulary. Chart semantics are not validated. These checks differ from the older per-model response serializer.

File inputs and results

An artifact is a stored file with a reusable handle.

Passing a file

Pass a previous output’s handle to another run using the same store:

{"csv": {"artifact": "<runId>/<index>"}}
Declared input Value received by Python
file Inline text, or stored file decoded as strict UTF-8
file with arrayBuffer: true Stored file bytes
image, audio, video Stored file bytes; MIME must match the declared media type and accept

Import a local file before a CLI run; use the returned handle in input.json:

jsee artifact-import data.csv --mime text/csv
jsee run schema.json --input input.json --json

Returning a file

For a declared file output, return text, bytes, bytearray or memoryview. To choose its name and MIME dynamically, return a dict under the output name:

def report():
    return {'report': {'content': b'a,b\n1,2\n',
                       'filename': 'report.csv', 'mime': 'text/csv'}}

Declare that output as {"name": "report", "type": "file"}. value and data also work as content keys. Paths and URLs are not opened as file content.

The response replaces the file with {artifact: "<runId>/<index>"}. Its entry in artifacts supplies id, output, name, mime, size, sha256 and uri.

File lifetime

Where it runs Storage and lifetime
CLI .jsee-artifacts/, or --artifact-dir; persistent files with file: URIs
HTTP Relative download URIs; files last until server shutdown
MCP Resource URIs; files last for the session

HTTP/MCP stores clean up on orderly close; a crash can leave temporary files. Handles cannot cross stores: download and upload the bytes when moving between servers or sessions. See file transfer rules for integrity checks, raw/stream exceptions and in-process artifact stores.

Routes and limits

Canonical serving adds these routes alongside the UI and per-model endpoints:

Route Purpose
GET /schema Inspect the computation descriptor
GET /openapi.json Read the whole-pipeline API description
POST /run Execute with a JSON input object
POST /__jsee/artifacts?name=data.csv Upload raw bytes; set MIME with Content-Type
GET /__jsee/artifacts/<runId>/<index> Download a stored file
Limit Default CLI / Python option
Request body 50 MiB --max-body-bytes / max_body_bytes
Artifact storage 64 MiB --max-artifact-bytes / max_artifact_bytes
Stored file count 1,024 Fixed

Input and output files share the quota; failed batches publish no partial files. Request limits also apply to per-model routes. Invalid inputs return 400, excessive bodies 413, unsupported content types 415, unsupported capabilities 422, model/output errors 500 and artifact quotas 507.

Canonical serving defaults to same-origin browser access. Configure authentication and CORS before sharing a server. These limits do not sandbox code or forcibly interrupt a running model.

MCP over stdio

MCP (Model Context Protocol) lets an agent discover and call your computation. Install the optional server dependencies with Python 3.10+:

python -m pip install 'jsee[server]==0.9.0'

Configure the MCP client to launch your Python environment’s absolute bin/jsee path with arguments ["serve", "/absolute/path/schema.json", "--mcp-only"]. The process uses stdin/stdout for MCP, stderr for model prints, and opens no HTTP port. Discovery reads the descriptor without importing model code.

  • Call: successful results contain the same output/artifact envelope as JSON CLI/HTTP, in structuredContent and JSON text. Failures set isError and return the failed envelope as text.
  • Read a file: use the returned resource link with resources/read. Check the decoded bytes against size and sha256.
  • Upload a file: use upload_artifact to send chunks, then pass the completed handle as an input. The shared MCP reference documents the chunk protocol and client setup.

Files use the same quotas for the session. Resource reads return entire files, so the client’s message limit may be lower than the storage quota. Python’s MCP SDK parses messages before JSEE checks decoded argument size.

The extra pins mcp 2.2.0. Remote MCP, streaming and forced cancellation are not supported; persistent model state across calls is not guaranteed. HTTP auth options do not apply to stdio process access.

API

jsee.serve(target, host='127.0.0.1', port=5050, **kwargs)

Start a server with a browser UI and HTTP API. canonical defaults to False, even when target is a schema file or dict:

Call HTTP surface
jsee.serve(target) Per-model routes, /api and /api/openapi.json; no canonical /run or /schema
jsee.serve(schema, canonical=True) Also exposes /run, /schema, /openapi.json and artifact routes
CLI jsee serve schema.json Enables canonical serving automatically

To embed the same server as the CLI, use:

import jsee

jsee.serve('schema.json', canonical=True, port=5050)

Use a local Python descriptor as in Use a schema file. canonical=True accepts a descriptor path or dict, but not a function. See routes and limits for request and response behavior.

target can be:

  • A function: schema auto-generated from type hints
  • A dict: pre-built JSEE schema object
  • A string: path to schema.json file

Function-to-schema options:

  • title: page title (default: function name)
  • description: page description (default: first line of docstring)
  • examples: list of dicts with clickable example inputs
  • reactive: True to auto-run on input change
  • outputs: dict or list of output type declarations
  • chat: True for chat mode (see below)
  • stream: True to serve sync or async generator results as SSE

Server options also include max_body_bytes, canonical, max_artifact_bytes, auth_token, auth_token_env and cors_origins; see limits and access settings.

from typing import Literal
import jsee

def calculator(num1: float, op: Literal['add', 'sub'], num2: float) -> dict:
    """A simple calculator"""
    if op == 'add': return {'result': num1 + num2}
    return {'result': num1 - num2}

jsee.serve(
    calculator,
    title='Calculator',
    examples=[{'num1': 3, 'op': 'add', 'num2': 4}],
    reactive=True
)

**kwargs does not create a form field. Extra request keys reach functions that accept it; other functions receive only declared parameters. Per-model routes remove legacy _jsee metadata unless the function explicitly declares it. Explicit caller and history parameters keep supplied values. Caller-provided schema dicts are copied before normalization.

Chat mode

Use chat=True to turn a function into a chat interface. The function receives message and history, returns a string response. The runtime accumulates messages and renders them as a chat conversation.

import jsee

def chat(message: str, history: list) -> str:
    """My chatbot"""
    return 'You said: ' + message

jsee.serve(chat, chat=True)

The browser keeps the conversation and sends {message, history} on each Run or Enter. history contains {role: 'user'|'assistant', content: str} records. JSEE awaits async functions, wraps string replies as {chat: response} and renders them as Markdown.

jsee.generate_schema(target, host='0.0.0.0', port=5050, **kwargs)

Generate a JSEE schema dict from a function without starting a server. Useful for inspecting or customizing the schema before serving.

import jsee

def predict(text: str, temperature: float = 0.7) -> str:
    return text.upper()

schema = jsee.generate_schema(predict)
# Customize schema
schema['inputs'][1]['min'] = 0.0
schema['inputs'][1]['max'] = 2.0
schema['inputs'][1]['type'] = 'slider'
# Save the browser descriptor for a separately served function
import json
schema['model']['url'] = 'http://localhost:5050/predict'
print(json.dumps(schema, indent=2))

Serve predict separately with jsee.serve(predict, port=5050). The generated descriptor calls that endpoint; editing it does not change a later serve(predict) call. For a customized native server descriptor, declare a local Python model file and pass that descriptor to serve().

Whole-pipeline invocation

Use await jsee.invoke_schema(schema, inputs, cwd=directory) to run a pipeline from Python without a server. schema is a dict; paths resolve relative to cwd. Use asyncio.run(...) from synchronous code.

Outcome Return value
Success {'runId': ..., 'status': 'succeeded', 'result': ...}
Failure {'runId': ..., 'status': 'failed', 'error': {'code': ..., 'message': ...}}

The result retains native values and intermediate data. Dict results merge between stages; scalars, lists and None replace prior data. A truthy stop skips later stages. This API does not serialize outputs or store returned files.

Inputs are validated and copied before importing models; defaults apply. Functions receive keyword arguments, or inputs in schema order with container: 'args'. Sync and async functions work. Capability limits follow model loading; errors distinguish invalid schema, invalid input, unsupported capability and execution failure. Synchronous code runs on the calling thread without a forced timeout or sandbox. See the clean sales example.

Server endpoints

These per-model routes are available for callable-based serving and schema-based apps. Canonical serving also provides /run and artifact routes:

Route Method Description
/ GET Interactive GUI
/api GET Schema and endpoint discovery
/api/openapi.json GET Auto-generated OpenAPI 3.1 spec
/{model_name} POST Execute model with JSON body

serve() uses a threaded dev server, so long model calls and open SSE streams do not block /api or static asset requests. Async functions are awaited before JSON serialization. Sync and async generator functions stream text/event-stream responses when stream=True.

Streaming results

Use a generator to send results while the function is still running. With stream=True, JSEE sends Server-Sent Events (SSE) and updates the browser as each value arrives:

import jsee


def stream_count(n: int = 5):
    for i in range(n):
        yield {'count': i}


if __name__ == '__main__':
    jsee.serve(stream_count, stream=True)

Save as stream.py and run python stream.py. Open http://localhost:5050, or use curl -N http://localhost:5050/stream_count -H 'Content-Type: application/json' -d '{"n":3}' to receive three data events followed by [DONE]. This uses the per-model route; JSON /run and MCP calls do not stream.

Generator failures after headers are sent produce an event: error frame with {"error": "..."}, followed by [DONE]. Put resource cleanup in finally: response write failures close suspended generators. A WSGI host must close the response iterable on disconnect. Code blocked inside computation still needs cooperative cancellation; disconnecting cannot forcibly stop it.

API discovery

/api lists the schema and endpoints. /api/openapi.json describes their inputs, declared outputs, examples and JSON errors. File inputs advertise multipart uploads; streaming models advertise text/event-stream.

# Call the quick-start function
curl -X POST http://localhost:5050/multiply \
  -H 'Content-Type: application/json' \
  -d '{"a": 3, "b": 4}'
# → {"result": 12.0}

# For your own analyze(data) function: upload a file (multipart/form-data)
curl -X POST http://localhost:5050/analyze \
  -F 'data=@myfile.txt'

# Discover endpoints
curl http://localhost:5050/api

# Get OpenAPI spec
curl http://localhost:5050/api/openapi.json

Return values

The per-model HTTP routes serialize results as follows. Canonical CLI/HTTP/MCP use the declared-output and artifact rules instead.

Python return JSON response
dict returned as-is (each value serialized individually)
int, float, str wrapped: {"result": value}
tuple converted to list: {"result": [a, b]}
bytes MIME-sniffed data URL, e.g. {"result": "data:application/pdf;base64,..."}
PIL Image base64 image (auto-detected)
list[dict] table format: {"result": {"columns": [...], "rows": [...]}}

The conversions apply to each top-level value in a returned dict. Declared scores outputs keep their row arrays, including aliases and grouped outputs. Non-finite numbers at any depth become JSON null on these per-model routes; canonical /run, CLI and MCP instead reject them.

WSGI deployment

For production deployment, use create_app() to get a standard WSGI application:

# app.py
import jsee

def multiply(x: float = 5) -> dict:
    return {'result': x * 2}

app = jsee.create_app(multiply)

Deploy with gunicorn, uWSGI, or any WSGI server:

python -m pip install gunicorn
gunicorn app:app

create_app() also defaults to per-model routes. Use jsee.create_app('schema.json', canonical=True) for the CLI’s /run and artifact API. It accepts the same targets and options as serve(), with the same model loading, validation and serialization. It awaits async functions and supports sync/async generators; see stream cleanup. Call app.close() when retiring a canonical application to release its artifact store.

HTTP authentication and CORS

Read a shared token from an existing environment variable:

jsee serve schema.json --auth-token-env APP_TOKEN
CLI option Python serve() / create_app() option
--auth-token-env APP_TOKEN auth_token_env='APP_TOKEN'
--auth-token TOKEN auth_token='TOKEN'
--cors-origin https://client.example (repeatable) cors_origins=['https://client.example']

Clients send Authorization: Bearer TOKEN. For the browser login, use username jsee and the token as password. Configured authentication covers every route, including static files and discovery; tokens stay out of HTML and model inputs. Authenticated responses use Cache-Control: no-store.

Canonical serving allows same-origin requests by default. Legacy serving allows wildcard CORS until authentication or an origin list is configured. cors_origins=[] / --cors-origin none allows same-origin only; wildcard '*' cannot be combined with authentication. Allowed preflights need no credentials, but actual requests do. Invalid credentials return 401; denied origins return 403.

Use HTTPS through a trusted proxy or an SSH tunnel. Explicitly allow the proxy’s public HTTPS origin; forwarded headers are not trusted automatically. See the shared host policy for exact origin syntax, preflight handling and OpenAPI security. These options also apply to legacy Python commands; they do not sandbox model code.

Offline

The package includes the browser runtime and selects core or full according to the output types. The built-in UI needs no CDN, but still needs the Python server running. For offline use, also provide your model dependencies, imports and data locally. This is not a standalone HTML export.

CLI: Python

jsee <target> [function] [data...] [options]
jsee init [template]

For inspect, validate, run, serve and file import, see JSON commands. The shorter serving forms below remain available.

Commands

jsee init [template]

Scaffold a new project. Templates: minimal (default), chat. Generates schema.json + model.py + README.md.

jsee init                   # minimal template
jsee init chat              # chat template

jsee <file.py> <function> [data...]

Serve a Python function as a web app with auto-generated GUI and REST API.

jsee app.py multiply                  # serve the quick-start function
jsee app.py multiply --port=8080      # custom port

jsee <schema.json>

Serve from a pre-built schema file.

jsee schema.json

Options

Flag Description
--host <addr> Host to bind to (default: 127.0.0.1; use 0.0.0.0 to expose on the local network)
--port <number> Port to listen on (default: 5050)
--max-body-bytes <number> Positive request body limit in bytes (default: 52428800, 50 MiB)

Data inputs

Positional args after the function name and --key=value args are mapped to function parameters. Values are auto-detected (numbers, JSON, file paths, strings). Inputs set from the CLI are locked in the GUI.

jsee app.py multiply 3 4               # positional data
jsee app.py multiply --a=3 --b=4       # named data
jsee schema.json 42 hello              # positional data with schema

Development

This package lives in the JSEE monorepo under py/.

# From the jsee repo root
npm install
npm run build
python -m pip install -e './py' pytest build wheel
python -m pytest py/test/ -v

For distributable packages, run npm run build:python from the repository root. The production build prepares both browser bundles and the descriptor schema before building the wheel and source distribution. Python package builds fail when any required asset is missing. Installing a prepared package requires no Node.js.

Run npm run test:python-native at the repository root to check native behavior and rebuild/install a wheel from its source distribution outside the checkout. These development checks require pytest, build, and wheel.

Versioning

Python and JavaScript share the 0.9 major/minor line; compatible fixes may have different patch versions.

Schemas accept an optional numeric schemaVersion: 1. An absent version keeps the same behavior; unsupported versions fail before importing model code. Descriptor format versions are separate from package versions.