JSEE
Portable, schema-driven web apps for local computation.
JSEE turns a model function plus a small JSON schema into a reactive browser app, CLI-served tool, offline HTML bundle and API surface. It is meant for computations that should be easy to share, inspect, rerun and keep on the user’s machine.
Contents
- Installation and quick start
- Complete example: browser, CLI, HTTP and MCP
- Schema, inputs, outputs and styling
- Browser API, renderers and embedding
- CLI and HTTP, authentication, MCP and offline bundles
- Python guide and Python CLI
- Status and limitations, development and changelog
Try it in the browser
Save this complete example as index.html and open it. Enter two numbers and click Run; 3 and 4 produce 12. This example loads JSEE from the CDN, then computes locally.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Multiply with JSEE</title>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@0.9.0/dist/jsee.core.js"></script>
<script>
function multiply(a, b) {
return a * b
}
new JSEE(multiply, '#app')
</script>
</html>
JSEE infers a simple form from this function. Use an explicit schema when you need types, defaults, descriptions, files or named outputs.
When JSEE fits
Use JSEE for calculators, data transformations, simulations and model interfaces. It supplies forms, file inputs, workers, progress, cancellation and output rendering. Browser apps can keep data on the device; bundling can make a computation portable as an HTML file.
It also embeds in an existing application, with host-owned controls and persistent renderers. The host still owns routing, accounts and collaboration. JSEE is not an application framework or a sandbox for untrusted code.
Installation
Browser: no build step:
<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@0.9.0/dist/jsee.core.js"></script>
JavaScript: CLI and Node.js execution:
npm install @jseeio/jsee
Use npx @jseeio/jsee to run the installed CLI. It requires Node.js 20+.
These docs describe 0.9.0. Pin package versions when reproducing a deployment. See runtime bundles for core versus full and development for installation from a checkout.
Quick start
JavaScript: create an app
In an empty directory:
npx @jseeio/jsee init
npx @jseeio/jsee schema.json --client
The first command creates schema.json, model.js and a README. Open http://localhost:3000 to use the generated form; --client runs its model in the browser. Stop the development server with Ctrl+C.
For a complete example you can call from the UI, CLI, HTTP and an MCP client, follow Your first computation. It includes every file and the expected results.
How it works
A model is the function, class or API that performs the work. A schema (also called a descriptor in this reference) declares the model, its inputs and its outputs. Browser JSEE creates editable controls, loads the model and routes returned values to matching output names. Models can run in workers and report progress; an array of models forms a pipeline. The browser API lets a host update controls, invoke a run, stop it and dispose of resources.
CLI, HTTP and MCP derive their input/output descriptions from the same schema. They validate values and return declared outputs. File and media results use artifacts: stored files returned as references with download metadata. A reference can be passed to another run using the same store. Models stay ordinary source files. A runtime must support the model’s language and execution features; moving a descriptor does not translate its code.
Your first computation
Build a calculator that multiplies two numbers and offers a text report. Use the same files as a browser app, a command-line computation, an HTTP API and an MCP tool. You need Node.js 20+ for the JavaScript version; the Python variation needs Python 3.9+.
Create the files
Create an empty directory and run npm install @jseeio/jsee. Save these two files there.
schema.json:
{
"schemaVersion": 1,
"id": "multiply",
"title": "Multiply two numbers",
"description": "Multiply a and b and produce a downloadable text report.",
"model": { "url": "model.js", "name": "multiply", "worker": true },
"inputs": [
{ "name": "a", "type": "float", "default": 3 },
{ "name": "b", "type": "float", "default": 4 }
],
"outputs": [
{ "name": "product", "type": "number" },
{ "name": "report", "type": "file", "filename": "result.txt", "mime": "text/plain" }
]
}
model.js:
function multiply({ a, b }) {
const product = a * b
return { product, report: `${a} × ${b} = ${product}\n` }
}
JSEE loads the function named by model.name in both the browser and Node. The model receives one object with input names and returns an object matching the declared output names. It does not need to know about forms, HTTP or MCP.
Open the browser app
npx @jseeio/jsee schema.json --client
Open http://localhost:3000 and click Run. The default inputs produce 12 and a report containing 3 × 4 = 12. Here --client selects browser execution, and the schema asks for a worker. Stop the server with Ctrl+C before the HTTP step below uses the same port.
Call it from the terminal
Save input.json:
{"a":3,"b":4}
Inspect the interface, validate the descriptor, then execute:
npx @jseeio/jsee inspect schema.json --json
npx @jseeio/jsee validate schema.json
npx @jseeio/jsee run schema.json --input input.json --json
Inspection shows the input/output JSON Schemas without loading the model. Validation checks the descriptor, not whether the computation is correct. Execution returns a JSON envelope like this (IDs, hashes and paths vary):
{
"runId": "...",
"status": "succeeded",
"outputs": { "product": 12, "report": { "artifact": ".../0" } },
"artifacts": [
{
"id": ".../0", "output": "report", "name": "result.txt",
"mime": "text/plain", "size": 12, "sha256": "...", "uri": "file:///..."
}
]
}
Read the file at the returned file: URI. Artifacts persist under .jsee-artifacts/ in your working directory. jsee run returns declared outputs only, omitting intermediate pipeline values. Models execute in Node for this step, not in browser workers.
Serve the computation
npx @jseeio/jsee serve schema.json --port 3000
Open http://localhost:3000 for the form, or use another terminal:
curl http://localhost:3000/run \
-H 'Content-Type: application/json' \
--data-binary @input.json
You get the same output/artifact envelope. In this mode the model runs on the server; the form and HTTP call share /run. The report URI is a relative HTTP download path. Fetch it from http://localhost:3000 while that server is running. Discovery is available at /schema and /openapi.json.
The server binds to localhost by default. For authenticated hosting, see HTTP authentication and CORS. Auth does not sandbox the model. Stop with Ctrl+C when finished; HTTP artifacts belong to that server lifetime.
Connect an agent through MCP
MCP (Model Context Protocol) lets an agent discover and call tools. In a client
that supports local stdio servers, add a server entry like the following. Replace /absolute/path/multiply/schema.json with the actual absolute path to your descriptor. Client configuration file locations differ.
{
"mcpServers": {
"multiply": {
"command": "npx",
"args": [
"--yes", "--package", "@jseeio/jsee@0.9.0", "jsee",
"serve", "/absolute/path/multiply/schema.json", "--mcp-only"
]
}
}
}
Restart or reconnect the client. Ask it to call multiply with a: 3 and b: 4. The tool returns product: 12, the report handle and artifact metadata. An MCP resource read retrieves the report bytes. JSEE uses stdin/stdout for the protocol and opens no HTTP port in this mode; do not send ordinary terminal input to that process.
The agent calls the computation described by your schema. JSEE does not need an LLM SDK or API key. Tool access and approval settings belong to your MCP client. See MCP over stdio for upload chunks, quotas and session lifetime.
Make an offline browser app
For the JavaScript version above:
npx @jseeio/jsee schema.json -o multiply.html --bundle=offline-strict
Open multiply.html with networking disabled and click Run. This example has no external model dependencies, so the generated file contains what it needs. The generated browser app runs locally; it does not include the Node HTTP or MCP server.
For models using API calls, external weights, dynamic imports or Python packages, check the bundle requirements. Native ES-module and Python HTML export have additional constraints. Host-owned renderers and editor closures are not serialized automatically.
Next steps
- Add a slider, choice, table, JSON or media input using the schema reference.
- Reuse this computation inside a larger page with the browser API.
- Build a file transformation with the CSV cleaning example.
- If initialization fails, check the browser console or server stderr. A failed canonical run has an
error.codeanderror.message; see error behavior.
Schema blocks
The package includes a JSON Schema for descriptor authoring (draft-07). Editors can use "$schema": "./node_modules/@jseeio/jsee/schema.json"; validators can load it with require('@jseeio/jsee/schema.json'). It checks known fields and nested groups without loading model code, and permits extension metadata. Runtime initialization retains its existing advisory checks for unknown options; authoring validation does not prove that a model or dependency can execute.
TypeScript authors can use the same vocabulary through the schema entry point:
import type { Schema } from '@jseeio/jsee/schema'
const schema = {
schemaVersion: 1,
model: { type: 'module', url: './model.js' },
inputs: [{ name: 'count', type: 'int', default: 3 }]
} satisfies Schema
These declarations cover descriptors and the pure input helpers exported from @jseeio/jsee/schema; they do not declare the browser JSEE class or CLI API. npm run test:types checks authoring examples and runs as part of npm test and the production build.
JSEE takes a schema object that contains three main blocks:
model: code location, language and execution optionsinputs: values the model receives, with types, defaults, validation rules and editing controlsoutputs: named results, their data types and how the browser displays them
Extra blocks can be provided for further customization:
id: optional stable computation identifier for discovery and OpenAPI, independent of the display title. Use 1-128 ASCII letters, digits,_,., or-, starting with a letter. Defaults tocomputationschemaVersion: optional descriptor format version. Use the number1; omitting it retains the same behavior. Unsupported explicit versions are rejected before model loading. This is independent of JS/Python package versionsrender/view: visualization part (optional). Defines custom rendering codedesign: optional layout, theme and color overrides-
imports: a list of scripts and stylesheets to load before the model is initialized. CSS files (.cssextension) are injected as<link rel="stylesheet">in<head>, JS files are loaded as scripts. In the browser, relative paths (e.g.dist/core.js,./lib.js) resolve against the page URL. With--bundle, the CLI resolves imports by checking the local filesystem first: if a file exists on disk it is bundled; otherwise it is fetched from CDN"imports": [ "https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js", "dist/my-lib.js", "styles/app.css" ] -
examples: optional sets of input values that users can select to fill the form"examples": [ { "input": "My name is Anton and I am" } ]
Schema
Model
Declare the code JSEE should run and how it receives inputs. A model object runs
one step; an array of model objects defines a sequential pipeline.
model: Contains main parameters of the model/scripturl(string): URL of a JS/Python script or POST/GET APIcode(function or string): It’s possible to pass code directly to JSEE instead of using an URLname(string): Name of the executed object. Default value is taken fromurlorcodetype(string, default:function): What kind of script is loaded. Influences how the code is initialized. Possible values:function: Call a JavaScript function with the current inputsclass: Create a JavaScript instance, then callmethod(default:predict) on each runmodule: Load an ES module URL with native dynamic import and callmodel.export,model.name,default, ormodel. Module models default toworker: false; setworker: trueto run the module in JSEE’s Web Worker. This is the JavaScript path intended forsecurity.allowEval: falseasync-function: Await an asynchronous JavaScript functionasync-init: Await a setup function once; it must return the function to call on each runpy: Run browser Python through Pyodide. Browser Pyodide models receive Python globalsinputsandctx. Useinputs['name']for input values andctx.get('caller')for runtime metadata. Legacyfrom js import namestill works for non-colliding names, but JSEE no longer overwrites existing JavaScript globals such aslocation,history,name,top, orparentget/post: Call an HTTP API model instead of local JavaScript/Python code. Usemodel.stream: truefor SSE responses- Inference note: when
codeis present (including code loaded from a.jsURL), JSEE treats the model asfunctionunlesstypeis explicitly set - TensorFlow.js is not a dedicated model type. Load it through
importsand call it from a normal JavaScriptfunctionorclassmodel
entry(string): For Python models, selects the callable independently of the endpointname. Modules load once per application. Pyodide functions can declare individual input parameters,inputs, and/orctx; native Python passes the declared request arguments.export(string): Fortype: "module"models, names the ES module export to call. JSEE triesexportornamefirst, then falls back todefaultormodelpyodide(string or object): Browser Python defaults to Pyodide 314.0.7 (Python 3.14.2). Override with an ES-module loader URL, or{ "url": "/pyodide/pyodide.mjs", "indexURL": "/pyodide/" }. Declareimportsorpackagesfor wheels included by--bundle=pyodide/--bundle=pyodide-single. Pyodide 314.x requires module workers; use its.mjsloader. Explicit older.jsloader overrides retain a classic-worker compatibility path; 0.24.1 is covered by a regression. Pyodide 314 bundles sqlite3/lzma, removes pydecimal/test packages and OpenSSL-backed hashlib algorithms, and stubs actual TLS operations in ssl. Browser fetch remains the HTTP path.method(string): Iftypeisclass,methoddefines the name of the class method to call during evaluationcontainer(string): How input values are passed to the function/method:object(default): Pass inputs wrapped in an object, i.e.{'x': 1, 'y': 2}args: Pass inputs as separate arguments- In
objectmode, JavaScript models receive the runtime context as a second argument:function model(inputs, ctx) { ... }
worker(boolean): Iftrue, JSEE initializes a Web Worker to run the scriptctx.log(...args): Write runtime logsctx.progress(value): Report progress (0..100ornullfor indeterminate)ctx.isCancelled(): Returns true once this invocation completes, is cancelled, or is replaced. Context callbacks belong to the invocation; latectx.log()/ctx.progress()calls are ignored. Browserctx.output()/ctx.outputAsync()follow the same rule. Browser instance fields and methods remain accessible through the context
timeout(number, default:30000): Worker execution timeout in milliseconds. Only applies whenworker: true. If exceeded, the worker is terminated with an error. Initialization has its own deadlineinitTimeout(number, default:120000): Worker initialization deadline in milliseconds, including runtime/import loading and async initialization. Failures and timeouts terminate the worker; increase explicitly for large models.workerUrl(string): Optional external worker entry, e.g./dist/jsee.worker.mjs, for a same-originworker-src 'self'deployment. Default creation remains dynamic: the bundled worker source becomes a Blob URL on HTTP(S), or a data URL for standalonefile://HTML. The classic entry isjsee.worker.js; use it for classic JS models and the compatibility modes below. Both files are built fromsrc/worker.jsand share execution, cancellation and messaging.
Python 314, native ES-module models and API models use module workers. Classic JavaScript functions/classes/imports keep classic workers, including
allowEval: falseURL scripts. ES-module models with classic JS imports also select that compatibility build; CSS imports do not. Explicit older Python.jsloaders use it too. Both builds share one worker implementation: no script concatenation, extra eval adapter or silent main-thread fallback. This preserves independent script strictness, global declarations and the existing CSP requirements.imports(array): Per-model imports loaded before this model’s code executes. Accepts URLs as strings or objects{ url: "..." }. Top-levelimportsare moved to the first model internallymodelcan also be an array of model objects to create a pipeline. Models execute sequentially: each receives the merged output of the previous one. First model defaults toworker: true, others toworker: false. Return{ stop: true }from any model to halt the pipeline early
render: Custom rendering script, for example to draw results with a plotting library. For a view that keeps selection or zoom between updates, see persistent output renderers
Design
Use design to set the app layout and appearance. Use setDesign to change them
after the app has loaded.
design: Design parameterslayout: Layout for the model/input/output blocks. If it’s empty and the JSEE container is not, JSEE uses inner HTML as a template. If the container is empty too, it uses the defaultblockstemplate. Set'sidebar'for a fixed-width (280px) sticky input panel: inputs stay visible while scrolling outputs. Collapses to single column on mobile.gridsets its column split; a host that places the app below its own header can bound the panel with the--jsee-sidebar-max-heightCSS variable (default100vh)framework: Design framework to use ('minimal'by default). If a JavaScript object with the same name is present in a global context, JSEE loads it too (using Vue’susemethod)theme: Visual preset: default minimal, or'mono'for monochrome controls, quiet headings and flat cards. Mono uses system colors by default. Existing'dark'selects the minimal dark palette;'auto'selects minimal with system colorscolorScheme:'light','dark'or'auto'; overrides the preset’s default. Auto follows system changes without JavaScript listeners. Applies separately to each app and its host regionsprimary: Accent color (buttons, toggles, gradient). Hex string, e.g.'#e74c3c'secondary: Second gradient color. Defaults to a darker shade ofprimarybg: Background color. Derives card, input, and border colors automaticallyfg: Text color. Derives secondary text color automaticallyfont: Font family string, e.g.'Georgia, serif'radius: Border radius in pixels (number) or CSS value (string)grid(array): 12-column grid split for input/output sections. E.g.[4, 8](default),[6, 6](equal),[3, 9]. Values arefrunits- All components use CSS custom properties that can be overridden via CSS:
--jsee-primary,--jsee-bg,--jsee-text,--jsee-text-secondary,--jsee-border,--jsee-input-bg,--jsee-input-border,--jsee-focus-border,--jsee-focus-ring,--jsee-card-bg,--jsee-bg-secondary,--jsee-label-bg,--jsee-error,--jsee-radius
Update appearance on an existing browser instance:
await app.setDesign({ theme: 'mono', colorScheme: 'auto' })
await app.setDesign({ colorScheme: 'dark' })
setDesign(patch) merges the existing reactive design state, waits for the DOM
update, and preserves inputs, workers and renderer instances. It accepts theme,
colorScheme, primary, secondary, bg, fg, font, radius, grid and
layout; null removes an override. It cannot replace framework. Invalid
patches apply nothing. getSchema() and exports include the current design.
Explicit design colors override preset colors. Changes affect only this instance,
not the host page or other apps; hosts own their theme switch and saved preference.
See examples/mono-theme.html for a standalone switch. No external fonts or CSS
are required.
Inputs
Each input declares a value the model receives and the control used to edit it.
Use name to match the model argument and type to choose the control.
inputs: Inputs definitionname: Key passed to the modeltype: Input control type:int,floatornumber: Numberstring: String (text input)color: Color picker (HTML5<input type="color">)text: Textarea (auto-resizes to fit content, up to 400px)checkboxorbool: Checkboxselectorcategorical: Select (one of manyoptions)slider: Range slider (min,max,step)range: Dual-handle range slider, returns[min, max]arrayradio: Radio button group (one of manyoptions)toggle: Toggle switch (boolean)date: Date pickermulti-select: Checkbox group, returns array of selectedoptionsgroup: Group of inputs. Useelementsarray for child inputs. Supports four display styles viastyleproperty:blocks(default): flat list of child inputsaccordion: collapsible section (also triggered bycollapsedorlabelprops)section: collapsible section with a prominent heading, for module-level groups in long formstabs: tabbed view where each child element becomes a tab (child groups use theirname/labelas tab label)
object: JSON editor for nested objects, lists or other JSON valuestable: Editable table using row objects or{columns, rows}. Clipboard CSV/TSV paste expands from the selected cell; choose whether an empty table takes its column names from the first pasted row. Duplicate headers reject without changing data. Unsafe integers and non-finite numeric text stay strings. This bounded editor does not ingest large CSV files or virtualize rows.image,audio,video: Typed media selection and preview; models receive bytesfile: File Inputfolder: Directory input. Shows a file listing with optional selection. In the browser, uses<input webkitdirectory>or drag-drop. On the server, reads from a directory path. Returns array of{name, path, size, type, selected}objects. Props:select(boolean): Show checkboxes for file selection (default: all files passed)reactive(boolean): Trigger model run on selection change
actionorbutton: Button (itsnamewill be passed as acallerto the model unless a computation input already declarescaller). Works as a top-level input or inside input groups
label(string): Text displayed beside the control; defaults tonameoptions(array): Choices for select/radio/multi-select inputs; values can be strings, numbers, booleans or objects such as{value: 'price', label: 'Price'}min,max,step(numbers): Numeric limits and step size;stepmust be positivedefault: Initial value when none is suppliednullable(boolean): Allow JSONnullas an input value. Anulldefault also permitsnull, preserving optional Python argument defaultsalias(string or array of strings): Alternative names for URL parameter matching. E.g."alias": ["f", "file"]allows?f=valueor?file=valueto set this inputdisplay(boolean or string): Show/hide the input directly, or use a Filtrex expression for a condition. Evaluated against current input values. E.g."display": "mode == 'advanced'"shows the input only whenmodeis"advanced". Supportslen()for string lengthreactive(boolean): Trigger a model run when this input changes (see when models run)disabled(boolean): Disables the input in the UI. Built-in input controls, selectors, folder selection checkboxes, and action buttons honor this flag. When combined withreactive: true, triggers an initial model run on page load (useful for server-populated values)readonly(boolean): Prevent editing while retaining the current value; controls without native readonly support are disabledaccept(string): File/media MIME types, wildcards or extensions, separated by commas; checked against file selection and stored artifact metadataenter(boolean): Iftrueon astringinput, pressing Enter triggers a model run (useful for chat or search inputs)raw(boolean, file input only): Iftrue, pass the raw source to the model instead of reading text in the UI (Fileobject for disk files or{ kind: 'url', url: '...' }for URL input)stream(boolean, file input only): Iftrue, pass an async iterableChunkedReaderto the model instead of raw source handles. Supportsfor await (const chunk of reader),await reader.text(),await reader.bytes(), andfor await (const line of reader.lines()). Works in both main-thread and worker execution. Reader metadata (reader.name,reader.size,reader.type) is preserved and remains available in downstream pipeline modelsvalidate(string): Browser control validation using Filtrex. The variablevalueholds the current input value. Expression must return truthy for valid input. E.g."validate": "value >= 0 and value <= 150". Runs on input changes (debounced) and before browser execution, including programmaticjsee.run()calls. Nested groups are traversed; group expressions receive their collected field values. Invalid controls show errors and block executionrequired(boolean): Ordinary browser controls reject empty strings, null, undefined, and empty arrays, including when avalidateexpression is also present. Table/JSON controls use field-presence semantics and permit empty collections as values. HTTP/CLI contracts use it for required field presence; portable value constraints come from declared types, options, and numeric boundserror(string): Custom error message forvalidateorrequiredfailures (default:"Invalid value"/"Required")columns(number, 1-12): Grid column span for this input. When any input hascolumns, the inputs section switches to a 12-column CSS grid. E.g."columns": 6makes the input take half width- URL params for file inputs (e.g.
?file=https://...) auto-load on init, so bookmarkable links run without an extra Load click
Outputs
Return values under the names declared in outputs. Each output type chooses a
browser presentation, such as a download button, table or chart. The
CLI/HTTP result rules describe serialization outside
the browser.
outputs: Outputs definition. Outputs also supportalias(string or array of strings) for matching model result keys by alternative names. Per-outputcolumns(number, 1-12) sets grid column span, same as inputsname: Key in the model resulttype: Output presentation type:-
file: Download-only file output. With a static schema filename, return the file body under the output name:{ "name": "report", "type": "file", "filename": "report.csv" }return { report: "col1,col2\n1,2\n" }For dynamic filenames, formats, or MIME types, return a descriptor object matching the output name:
return { report: { filename: "dataset.csv", content: csvText, mime: "text/csv" } }Browser descriptor fields:
filenameorname;content,value,data, orurl; optionalmimeorcontentType. CLI/HTTP/MCP file results require content; they do not fetch aurl. object: JSON tree for objects, lists and other JSON valueshtml: HTML markup; sanitization followssecurity.allowRawHtmlsvg: Inline SVG markup; sanitization followssecurity.allowRawHtmlcode: Code blockmarkdown: Rendered Markdown (supports tables, headings, lists, etc.)image: Image (<img>tag from data URL or URL)audio: Audio player (<audio>from URL or data URL)video: Video player (<video>from URL or data URL)table: Virtualized table with scrollingchat: Chat message list. Accumulates{role, content}messages across runs instead of replacing. Renders user/assistant bubbles with Markdown support, auto-scrolls to latest message. See Chat mode belowgroup: Group of outputs. Useelementsarray for child outputs. Supportsstyle: 'tabs'for tabbed display or default blocks (stacked). Child outputs are matched by name against model resultschart: SVG chart via Observable Plot. Model returns array of objects, column-oriented data, or a full Plot config. Schema props:mark(line/dot/bar/area/etc.),x,y,color,width,height. Included in full bundle or load Plot viaimports3d: 3D model viewer via Three.js. Model returns a URL (GLTF/GLB), data URL, or geometry object{vertices, faces}. Schema props:width,height. Included in full bundle or load Three.js viaimportsmap: Interactive map via Leaflet. Model returns markers[{lat, lng, popup}],{center, markers, zoom}, or GeoJSON. Schema props:height,zoom,center,tiles. Included in full bundle or load Leaflet viaimportspdf: Browser-native PDF viewer. Model returns a URL, data URL, Blob, ArrayBuffer, or Uint8Array. Schema prop:height. Included in the core bundlegallery: CSS grid of images. Model returns array of URLs/data URLs. Click to expand lightbox. Schema props:columns(default 3),gap(default 8px). Included in the core bundlehighlight: Highlighted text with labels. Model returns[{text, label, color}]segments. Unlabeled segments render as plain text. Included in the core bundlescores: Ranked labels with numeric scores and bars, for example classifier predictions. See ranked scores for data and display options.gauge: Semicircle gauge. Model returns a number or{value, label}. Schema props:min(default 0),max(default 100),label,color. Included in the core bundlenumber: Large KPI number display. Model returns a number or{value, delta, label}. Delta shown with colored up/down arrow. Schema props:label,prefix(e.g."$"),suffix(e.g."%"),precisionalert: Colored status banner with left accent border. Model returns a string or{message, type}. Four variants:info(blue, default),success(green),warning(amber),error(red). Schema prop:alertType(default type when value is a string)viewer: Preview a media URL. Model returns a URL string. Auto-detects content type from extension: images (png, jpg, gif, svg, webp), audio (mp3, wav, ogg, flac), video (mp4, webm, mov), or iframe (fallback). Useful withfolderinput for file previewfunction: Render function. Rather than returning a value, a model returns a function that JSEE will call passing the container elementblank: Empty container for custom rendering viafunctionoutput or external code
-
Ranked scores
Use type: "scores" to compare labeled values, such as classifier predictions.
Return an array like [{label: 'cat', score: 0.72}, {label: 'dog', score: 0.28}].
The view sorts rows and displays bars; CLI/HTTP/MCP retain the original order,
numbers and extra row metadata. Empty arrays are valid; labels must be strings
and scores finite numbers.
| Option | Display behavior |
|---|---|
min, max |
Bar scale, default 0-1; both must be finite and increasing |
format |
number (default) or percent; does not change the stored value |
precision |
Maximum decimal places, 0-20; default 3 |
prefix, suffix |
Text around the displayed value |
Out-of-range scores clip the bar and are identified in accessible text. The view does not normalize scores, interpret them as probabilities or record history. It updates in place, inherits the app theme and is included in the core/offline runtime without a chart library. See the scores example.
When models run
By default, the model runs when the user clicks Run. Three schema-level options change this:
autorun(boolean, default:false): run the model once on first load, then wait for manual Run clicksreactive(boolean, default:false): re-run the model on any input change (debounced 300ms). Useful for lightweight models where instant feedback is desiredinterval(number, default:0): after an autorun or manual Run, repeat execution every N milliseconds until Stop is clicked.0means no repetition. The first run keeps its originalcallervalue (autorunorrun); scheduled repeats usecaller: "interval"- Per-input
reactive: setreactive: trueon an individual input to trigger a model run only when that specific input changes (fires immediately, not debounced). Other inputs still require a manual Run click. Useful when one input is interactive (e.g. a slider) but others are expensive to recompute
| Option | Scope | When it runs | Debounced |
|---|---|---|---|
autorun: true |
schema | once on load | no |
reactive: true |
schema | every input change | yes (300ms) |
reactive: true |
input | that input changes | no |
interval: N |
schema | after autorun or Run, then every N ms | no |
Chat mode
Declare an output with "type": "chat" to enable conversational UI. The runtime manages state:
- User types a message in a text input and presses Run (or Enter if the input has
"enter": true) - The runtime injects
history(array of{role, content}dicts) into the model payload alongside the user’smessage, unless a computation input already declareshistory - The model returns
{chat: "response string"}(or{chat: {role, content}}) - The runtime appends both the user message and assistant response to the chat conversation, clears the input, and auto-scrolls
- Messages render with Markdown support (code blocks, bold, links, tables)
Works with browser workers and server POST models. For Python, see chat mode.
Saving input values
By default, JSEE saves input values to localStorage and restores them on page refresh. Values are namespaced by app/container/input shape, include grouped inputs, skip file/folder/action/button/disabled inputs, and are validated against type/options/min/max before restore. Priority: URL params > localStorage > schema defaults. Set persist: false in the schema to disable. Clicking Reset clears saved values
Notifications
Set notify: true in the schema to show a browser notification when a run completes while the tab is hidden. JSEE requests permission on first load and fires new Notification() after successful runs only
Security and CSP
security.mode: "secure" or security.trusted: false defaults the flags below to
false. Explicit booleans, such as security.allowNetwork: true, override those
defaults.
Flag set to false |
Blocks or changes |
|---|---|
allowEval |
Inline JS/Python code, cached hidden JS assets, Blob-script execution, worker class lookup and Pyodide source execution |
allowNetwork |
JSEE-managed API requests and network model/import/Pyodide loads |
allowRawHtml |
Sanitize HTML, SVG, Markdown and chat; remove raw Markdown HTML, event handlers, scripts and unsafe URLs |
allowNotifications |
Skip notification permission requests and completion notifications |
allowFileViewer |
Disable file/folder controls; replace file/media outputs with a blocked placeholder |
For JavaScript without eval, use type: "module" with a module URL. It runs on the
main thread by default; set worker: true for worker execution. Relative
model/import URLs are checked before resolution, so a same-origin relative URL is
not treated as an external network URL. Loading a schema URL happens before its
security settings can be known.
These flags control JSEE-managed operations. They do not restrict arbitrary model code, all network calls, the host filesystem, CPU or memory. Workers run on a separate thread but retain capabilities such as network access. The host must provide any required execution sandbox.
Content Security Policy (CSP)
For deployments that prohibit eval, prefer security.mode: "secure" plus type: "module". The current browser bundle injects built-in CSS at runtime, so built-in UI styles require style-src 'unsafe-inline'. Dynamically embedded workers require worker-src blob: over HTTP(S), or worker-src data: for file-origin HTML. An external model.workerUrl can use worker-src 'self'
Baseline same-origin module policy:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:;
connect-src 'self';
worker-src 'none';
object-src 'none';
base-uri 'none';
form-action 'none'
Use worker-src blob: instead of worker-src 'none' for worker: true module models. If the runtime, module URL, imports, images, API endpoints, or Pyodide assets come from another origin, add those origins to the matching CSP directive and keep security.allowNetwork aligned with that choice.
| Schema path | Required CSP additions | No unsafe-eval |
Notes |
|---|---|---|---|
type: "module", worker: false |
script-src must allow the module URL |
yes | Verified with security.mode: "secure" and worker-src 'none' |
type: "module", worker: true |
worker-src blob: (or self with workerUrl) and script-src must allow the module URL |
yes | Embedded and external entry points are tested |
Main-thread JavaScript model.code string |
script-src 'unsafe-eval' |
no | Blocked by security.allowEval: false |
Worker classic JavaScript model.code string |
worker-src blob: plus script-src blob: |
no | Blocked by security.allowEval: false; class lookup also uses eval |
| Hidden bundled JavaScript model/import code | Same as inline code | no | Intentionally blocked when security.allowEval is false |
| Remote API/model/import/Pyodide URLs | Matching connect-src, script-src, or worker/runtime directives |
depends on model type | Blocked by security.allowNetwork: false unless explicitly allowed |
Streaming results
Use Server-Sent Events (SSE) to display results as they arrive, such as generated
text or successive measurements. Set model.stream: true for progressive GET/POST results on the main thread or in a worker. SSE frames join multiline data: fields and preserve null, false, 0, and empty strings. [DONE] completes the invocation and releases its reader without waiting for the server to close; event: error rejects the run. Unsuccessful HTTP responses also reject, while ordinary data objects containing an error key remain model results. The last data frame is the pipeline result; EOF without a sentinel remains accepted, including legacy unterminated final frames. A presentation callback is optional. Node serving can stream sync/async JavaScript generators; see the Python streaming example for native Python. Streaming uses browser/API models and legacy stage endpoints, not the JSON /run operation.
Node and Python servers send generator failures as event: error frames followed by [DONE] once SSE headers have been sent. They close suspended generators when a response write detects disconnection; Node also releases waits for response backpressure. Producers blocked inside computation still need cooperative cancellation. Rendering copies result records before consuming metadata, so streamed pipeline fields such as stop survive. Chat records the user message once per run and does not append the last streamed assistant chunk again at completion; each partial chunk is a message, not an inferred text delta
Binary data and typed arrays
Browser image outputs convert data URL strings longer than 50,000 characters
to Blob URLs and revoke previous Blob URLs on updates.
In browser execution, declare arrayBuffer: true on an input to convert numeric
arrays to typed arrays before passing them to JavaScript models or WebAssembly code. The example below uses a
JSON editor to collect an array such as [1, 2, 3]. Set dtype to control the type (float32, float64, uint8, int32, etc., default: float64). Typed-array conversions preserve numeric values and subarray boundaries; raw ArrayBuffer/DataView inputs retain byte interpretation. Worker requests use structured cloning so the caller can reuse its buffers on subsequent runs. Node invocation leaves numeric arrays as arrays; convert them in model code if needed. This differs from reading file artifacts as binary bytes.
"inputs": [{ "name": "data", "type": "object", "default": [1, 2, 3], "arrayBuffer": true, "dtype": "float32" }]
Cancellation and validation
Call jsee.cancelCurrentRun() on the JSEE instance to request stop of the active run. Long-running models should check ctx.isCancelled() and return early:
async function model (inputs, ctx) {
for (let i = 0; i < inputs.steps; i++) {
if (ctx.isCancelled()) throw new Error('Cancelled')
await doStep(i)
ctx.progress(Math.round(100 * (i + 1) / inputs.steps))
}
return { done: true }
}
- Schema validation: JSEE validates schema structure during initialization and logs warnings for non-critical issues (e.g. unknown input types, malformed aliases)
- Invocation validation: Browser runs, CLI runs, and Node/Python model endpoints check declared types, option values, numeric bounds/steps, and required input presence before invoking a model. Invalid HTTP inputs return 400. APIs apply explicit defaults; UI/legacy CLI controls also retain their type-based initial values.
required: falsepermits omission;required: truerequires the field even when a default is advertised. UI-only validation expressions and nonempty-field checks remain control behavior
Concurrent runs
Worker rebuilding reserves the run slot: overlapping automatic runs are dropped and manual runs queue. Stop aborts model-source fetches during rebuilding; stopped or failed rebuilds remain retryable and release partially initialized workers. A cancelled run cannot enter its first or a later pipeline stage. Main-thread initialization that cannot be interrupted keeps the slot until it settles. Invocation contexts guard supported callbacks; direct instance mutation and legacy raw worker postMessage() callbacks carry no such guard
Input groups and reserved metadata
Named groups become nested objects; unnamed layout groups flatten into their containing object. Buttons are excluded. Option objects use value, then name, then label; numeric and boolean options retain their types. Positional JS models (container: 'args') receive declared inputs in schema order, without extra caller metadata
Browser caller and chat history injection never replaces declared computation inputs. Browser and CLI output lookup accept names, sanitized names, and string/array aliases inside output groups. Fields named caller, stop, _status, _log, and _progress stay hidden from automatic output discovery, but explicit output names/aliases can present them. A truthy stop still halts the pipeline, even if displayed as an output
Raw postMessage() progress/partial callbacks are accepted while the worker stage is pending and ignored after it settles. They have no invocation identity: a delayed raw message arriving during a later call cannot be attributed safely. Use invocation-bound ctx.log()/ctx.progress() for asynchronous callbacks; raw messages do not have their lifetime guarantee
Instance state and inferred inputs
JSEE copies descriptor containers before normalization and keeps mutable UI values separate. Reusing the same schema across instances does not share input values. Verbosity and embedded log sinks belong to each instance
When schema.inputs is omitted and JSEE infers inputs from JavaScript function parameters, inferred inputs default to string for compatibility. JSEE logs warnings for inferred string inputs, numeric defaults, JSDoc numeric hints, and unsupported destructured parameters so authors know when to declare schema.inputs explicitly. Set top-level inferInputTypes: true to opt into inferred number controls from numeric defaults or numeric JSDoc and checkbox controls from boolean defaults or JSDoc
Exporting browser apps
jsee.download(title) exports an HTML app, with title defaulting to 'output'.
Offline use depends on the declared models and dependencies; see
bundle requirements and renderer export restrictions.
Generated page metadata
The CLI uses page to set metadata on generated HTML:
title(string): Page titleurl(string): Page URLga(string): Google Analytics measurement ID (e.g."G-XXXXXXXXXX"). Invalid IDs are omitted from generated HTMLsocial(object): Social media links:twitter,github,facebook,linkedin,instagram,youtube(values are usernames/handles)org(object): Organization footer:name,url,description
Smart output auto-detection
In the browser, JSEE can choose a view from a returned value. Declare output types explicitly when you need stable CLI, HTTP or MCP result schemas.
When outputs are not explicitly defined in the schema, or when a model returns keys that don’t match any named output, JSEE infers the output type from the value:
| Value | Inferred type |
|---|---|
| Array of objects | table |
Array of image URL strings (.png, .jpg, .gif, .svg, .webp) |
gallery |
| Other arrays | object |
data:image/* string |
image |
data:audio/* string |
audio |
data:video/* string |
video |
data:application/pdf string |
pdf |
String ending in .png, .jpg, .gif, .svg, .webp |
image |
String ending in .mp3, .wav, .ogg, .flac |
audio |
String ending in .mp4, .webm, .mov |
video |
String ending in .pdf |
pdf |
String ending in .md |
markdown |
| String with newlines and > 200 characters | code |
| Other strings | string |
| Numbers, booleans | string |
| Objects | object |
Structured and media inputs
Use these inputs for data that does not fit a single text or number field.
JSON objects and editable tables
object accepts JSON values, including nested objects and variable-length lists. Its editor reports invalid JSON and blocks execution until corrected. table accepts the same values as table outputs: an array of row objects or { "columns": ["name"], "rows": [["Ada"]] }. The table editor supports adding/removing rows, adding columns and editing JSON. Cells preserve JSON types: 0, false, null and "" are distinct. Column-specific validation is not inferred. Use an optional valueSchema to validate the structure of an object input or
output. For example, require a counts array of nonnegative integers:
{
"name": "info",
"type": "object",
"valueSchema": {
"type": "object",
"properties": {
"counts": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0
}
}
},
"required": [
"counts"
],
"additionalProperties": false
}
}
valueSchema uses a bounded JSON Schema vocabulary: type (including type arrays and null), properties, required, boolean additionalProperties, items, enum, minimum, maximum, minLength, maxLength, minItems, maxItems, and description. Browser, Node and Python enforce these rules, and HTTP/MCP discovery exposes them. Unsupported keywords fail descriptor validation; references, formats, nested defaults and coercion are not supported. When valueSchema is present, it determines whether null is allowed: include "null" in its type if needed; outer nullable or default: null do not widen it. Without a shape, object keeps accepting any finite JSON value.
Use named input groups when each field should have its own control; use object
when users need to edit JSON directly.
Image, audio and video files
image, audio and video provide file selection and previews. Declare accept, such as "image/png" or "audio/*", to restrict selection; the media type also requires its own MIME family. Browser JS models receive an ArrayBuffer; native Python models receive bytes. Pyodide models can use bytes(value) on the received buffer. Canonical CLI/HTTP/MCP inputs use { "artifact": "<runId>/<index>" }, rather than encoded media strings. MIME describes supplied metadata; it does not prove that a codec can decode the content. JSEE does not implicitly convert images to PIL/NumPy or audio to sample arrays.
Media outputs may return bytes (with mime in the output descriptor), or {content: bytes, mime: "image/png"}. Canonical hosts store these as reusable artifacts. Existing media URL strings remain URLs. The canonical served UI uploads selected media, invokes the whole pipeline through /run, and displays its artifacts. Standalone browser apps keep local file data on the device. These editors support disabled and readonly.
Runtime Bundles
| Bundle | Use when | Includes |
|---|---|---|
dist/jsee.core.js |
Most apps, direct <script> tags, generated HTML |
runtime, inputs, core outputs, native PDF viewer |
dist/jsee.full.js |
Apps that need chart, 3d, or map without extra imports |
core + Observable Plot + Three.js + Leaflet |
@jseeio/jsee package entry |
CLI/server-side tooling | Node CLI module |
Full bundle CDN:
<script src="https://cdn.jsdelivr.net/npm/@jseeio/jsee@0.9.0/dist/jsee.full.js"></script>
The core bundle can still render chart, 3d, and map if the app loads the required library through schema imports. The pdf output uses the browser’s native PDF viewer and is available in the core bundle. The CLI and Python server choose the required runtime bundle from the schema output types.
Examples
Full schema
These are reference fragments, not standalone programs. Supply the model files named by each descriptor. Start with the complete computation tutorial for runnable files.
This descriptor combines multiple inputs, outputs, theming, and a worker model:
{
"model": { "url": "model.js", "worker": true },
"design": { "primary": "#2a7ae2", "grid": [4, 8] },
"inputs": [
{ "name": "count", "type": "slider", "min": 10, "max": 1000, "default": 100 },
{ "name": "method", "type": "select", "options": ["linear", "quadratic"], "default": "linear" }
],
"outputs": [
{ "name": "summary", "type": "number", "prefix": "n = " },
{ "name": "data", "type": "chart", "mark": "dot", "x": "x", "y": "y" },
{ "name": "raw", "type": "table" }
],
"autorun": true
}
Chart output
Model returns array of objects or column-oriented data:
function model ({ count }) {
const data = Array.from({ length: count }, (_, i) => ({
x: i, y: Math.sin(i / 10) + Math.random() * 0.2
}))
return { chart: data }
}
Schema: { "name": "chart", "type": "chart", "mark": "line", "x": "x", "y": "y" }
Map output
Model returns markers array:
function model () {
return {
map: [
{ lat: 51.505, lng: -0.09, popup: 'London' },
{ lat: 48.857, lng: 2.352, popup: 'Paris' },
{ lat: 52.52, lng: 13.405, popup: 'Berlin' }
]
}
}
Pipeline
Sequential models pass merged results forward:
{
"model": [
{ "url": "preprocess.js", "worker": true },
{ "url": "analyze.js", "worker": false }
]
}
First model returns { cleaned: [...] }, second receives { ...originalInputs, cleaned: [...] }. Return { stop: true } from any model to halt early.
Multi-column layout
Use columns on inputs/outputs for dashboard-style layouts:
{
"inputs": [
{ "name": "a", "type": "slider", "min": 0, "max": 100, "columns": 6 },
{ "name": "b", "type": "slider", "min": 0, "max": 100, "columns": 6 }
],
"outputs": [
{ "name": "total", "type": "number", "columns": 4 },
{ "name": "chart", "type": "chart", "columns": 8 }
]
}
JSEE instance API
Use the instance API to read inputs, run a model or embed JSEE in another page.
Create an instance with new JSEE({schema, container}) and await ready before
interacting with it. Multiple instances can share a page; each owns its inputs,
progress display and overlays.
| Method | Description |
|---|---|
await jsee.ready |
Wait for schema, model, UI and pipeline initialization; rejects on failure or destruction |
await jsee.setDesign(patch) |
Change theme, color scheme and layout without recreating the app; see design |
jsee.getInputs() |
Read a snapshot of input values after readiness; named groups remain nested |
await jsee.setInputs(values) |
Patch input values by schema name, including nested groups; unknown names reject atomically. Does not run unless schema reactivity is enabled |
await jsee.setInputState(path, patch) |
Update disabled, readonly, boolean display or choice options without changing values or triggering a run |
await jsee.invoke(values?) |
Run once and return declared output values (or the pipeline result when no outputs are declared). Rejects on validation, execution failure, overlap, or cancellation (AbortError) |
jsee.on(event, listener) |
Subscribe to inputs, start, result, error, cancel, destroy, stage, or progress; returns an unsubscribe function. Listener exceptions do not affect execution |
await jsee.stop(options?) |
Stop and wait for invocation settlement; forced by default. Opt into cooperative cancellation with { mode: 'cooperative', graceMs: 1000 }; returns { retained } |
jsee.run(caller) |
Execute the model pipeline. caller can be 'run', 'autorun', 'reactive', 'interval', or a custom button name |
jsee.output(result) |
Process and render results to output cards |
jsee.cancelCurrentRun() |
Stop the active run. Rejects the current run promise, sets cancellation state, and signals/terminates workers |
jsee.isCancelled() |
Returns true if cancellation was requested |
jsee.progress(value, details?) |
Report progress: 0-100 for determinate, null for indeterminate; optional loaded, total and phase metadata |
jsee.download(title) |
Export the current app; see offline requirements |
jsee.destroy() |
Cancel runs and initialization fetches, terminate workers, unmount Vue, and release UI resources. Repeated calls are safe; later runs return false and late progress/output calls are ignored |
jsee.notify(text) |
Show a success toast notification |
jsee.log(...args) |
Log to browser console and optional #log DOM element as plain text |
Cooperative cancellation
stop() still terminates workers by default. To retain a loaded model when its
operation can finish cleanup, use:
const { retained } = await tool.stop({ mode: 'cooperative', graceMs: 1000 })
// The cancelled invoke() rejects with AbortError. After stop() resolves,
// another invocation can reuse the model when retained is true.
Browser JavaScript model contexts expose ctx.signal, an AbortSignal for the
current invocation, alongside ctx.isCancelled(). Pass it to abortable work:
async function model(data, ctx) {
const response = await fetch(data.url, { signal: ctx.signal })
return { text: await response.text() }
}
For batched computation, yield to the event loop and check cancellation between
batches. A worker cannot receive cancellation while synchronous code blocks its
message loop. Cooperative Stop suppresses subsequent outputs/progress, prevents
downstream stages from running, and keeps the run slot occupied until the model
settles. Cleanup belongs in the model’s finally; completed mutations are not
rolled back automatically. retained reports runtime retention; the adapter
owns recovery and whether its model state remains usable.
The grace period defaults to 1000 ms. If an active worker has not settled, JSEE
falls back to forced Stop, discards its workers and returns retained: false.
Main-thread JavaScript cannot be preempted: cooperative Stop waits for it to
settle even beyond the grace period. Run potentially blocking models in workers.
Call cooperative Stop after ready; before readiness it rejects without claiming
retention. Use destroy() to dispose initial setup. Reinitialization has no
cancellable invocation context and uses forced teardown. Explicit stop() or destroy() can escalate a pending
cooperative stop; destroy always releases the instance’s resources. Native
runtimes need not supply ctx.signal.
Host control state and progress
Use setInputState to change controls without changing their values. Subscribe to
progress when the host supplies its own progress display.
const tool = new JSEE({ schema, container: '#app', progress: false })
await tool.ready
await tool.setInputState('model', { disabled: true })
await tool.setInputState(['settings', 'target'], {
options: [{ value: 'price', label: 'Price' }]
})
const unsubscribe = tool.on('progress', event => {
// { runId, stageId, mode, value, loaded?, total?, phase? }
showProgress(event)
})
// In a model: ctx.progress(50, { loaded: 1024, total: 2048, phase: 'load' })
Updating controls
| Patch property | Effect |
|---|---|
disabled |
Disable a field |
readonly |
Use native readonly behavior where available; disable selectors, sliders and other controls without it |
display |
Show or hide a field or group |
options |
Replace the options of a choice input; retain its current value |
A string path is a literal field name, including dots. Use an array such as
['settings', 'target'] to traverse named groups. Unnamed groups share their
parent’s namespace; named pipeline scopes are input groups.
- Groups accept only
display; update their individual fields fordisabledorreadonly. - Invalid paths or patches reject before any state changes.
- Patches preserve values, emit no
inputsevent, and do not run reactive models or invalidate stage caches. - If new options exclude the current selection, validation fails until the user or host changes the value with
setInputs.
Control state does not bypass validation or grant permission to execute.
Reading progress
The constructor option progress: false hides JSEE’s progress bars but keeps
emitting events. It is independent of controls and toasts.
| Event field | Meaning |
|---|---|
runId |
Per-instance invocation ID; null outside an invocation |
stageId |
Named stage ID, or null for an unnamed stage |
mode |
determinate or indeterminate |
value |
Clamped percentage, or null for indeterminate progress |
loaded, total, phase |
Optional metadata supplied by the model |
Use the invocation’s result, error or cancellation to detect completion; progress 100 does not establish readiness. The runtime may emit zero when resetting after a worker call.
Report through ctx.progress inside models: JSEE associates it with the current
invocation and ignores reports from retired contexts. Legacy manually posted
worker messages without request IDs remain supported, but late reports cannot be
reliably attributed. For stopping a run while retaining its model, see
cooperative cancellation.
Image acquisition in browser apps
An image input accepts upload, drop and paste into its focused control. Paste
inside a text field stays text. Enable camera snapshots in the same descriptor:
{"name":"photo","type":"image","accept":"image/*","capture":{"camera":true,"live":true,"maxFps":5}}
The generated control offers Camera, Use snapshot and, for ordinary pipelines,
Live. Camera access requires browser permission on a secure origin. facingMode
is optionally "user" or "environment" (default). maxFps defaults to 5 and
must be greater than zero and at most 60. Actual throughput depends on capture
and execution time. Snapshots use PNG, JPEG or WebP according to accept.
Capture in a custom layout
Custom layouts use the same acquisition implementation:
await app.ready
const image = app.createImageInput('photo', {
target: document.querySelector('#image-stage'), // scoped paste/drop target
picker: document.querySelector('#image-file'), // optional native file input
video: document.querySelector('#camera'), // required for camera capture
onImage: file => showPreview(file), // selection is already stored
onState: ({ active, pending, live }) => updateCameraButton(active, pending, live),
onError: error => showError(error.message)
})
await image.start({ live: true })
await image.stop({ snapshot: true }) // release camera; keep its last image
image.dispose() // remove listeners and release tracks
Use an array path such as ['settings', 'photo'] for an input in a named group.
| Method | Behavior |
|---|---|
select(files) |
Validate and store a selection, as upload/paste does |
start() |
Open a camera preview for snapshots |
start({live: true}) |
Start live capture; requires capture.live |
stop() |
Release camera tracks immediately, then wait for an in-flight frame consumer |
stop({snapshot: true}) |
Also keep the last image |
dispose() |
Remove listeners and release tracks |
Disabling the input, making it readonly, stopping the runtime, hiding the page or
disposing the control stops capture. App destruction disposes its acquisition
instances. Stopping capture alone does not cancel a model run; use app.stop()
for execution.
Processing live frames
Live capture waits for the previous frame consumer before sending another frame;
it does not accumulate a queue. The default consumer calls an ordinary pipeline
with app.invoke(values, {transient: true}): values are validated for this run
without changing stored form inputs.
Supply onFrame: async file => ... to handle frames yourself. Named pipelines
require this callback to choose a target; their generated controls offer snapshots
only. Do not await stop() or select() inside onFrame, because those methods
wait for the callback to settle.
Bitmap transport and ownership
Frames are encoded files by default. Set
capture: {camera: true, live: true, transport: 'bitmap'} to receive decoded
ImageBitmap live frames. Snapshots and uploads remain files, so the model must
handle both types. Bitmap execution supports local browser JavaScript; Python,
HTTP, CLI and MCP use files or artifact references.
| Bitmap source | Owner and lifetime |
|---|---|
Passed to app.invoke({photo: bitmap}, {transient: true}) |
Caller owns the original; each model call borrows a separate copy |
| Received by a model | JSEE closes the copy after settlement, including failures; worker delivery transfers only this copy |
Received by onFrame |
Valid until the callback’s returned promise settles; JSEE also closes late frames after Stop/disposal |
A model may close its copy early. To retain it, make another copy; return files or pixel data rather than the borrowed bitmap. Forced worker termination disposes the worker. Ordinary buffers retain their existing cloning behavior.
For named stages, use
invoke(undefined, {target, transient: true, overrides: {photo: bitmap}}).
Frames do not enter retained stage results. Bitmaps cannot be stored with
setInputs or sent over JSON; there is no automatic fallback to another input
type. Bitmap transport avoids PNG encoding/decoding on the live path but does not
guarantee zero-copy execution or a particular speed. Models still own
preprocessing and inference.
Persistent output renderers
Pass a renderers map when constructing an embedded app, and select a name with
output.renderer. The output keeps its computation data type: workers return
ordinary data, and CLI/HTTP/MCP keep the same declared output schema.
function countView(container, initial) {
const label = document.createElement('span')
container.append(label)
const update = data => { label.textContent = String(data.count) }
update(initial)
return {
update,
dispose() { /* release listeners, observers or graphics resources here */ }
}
}
const app = new JSEE({
container: '#app',
renderers: { count: countView },
schema: {
inputs: [{ name: 'n', type: 'int', default: 1 }],
model: { worker: true, code: ({ n }) => ({ result: { count: n * 2 } }) },
outputs: [{ name: 'result', type: 'object', renderer: 'count' }]
}
})
Renderer lifecycle
The factory is called as renderer(container, initialData) and returns
{ update(data), dispose?() }.
| Action | What JSEE calls or preserves |
|---|---|
| First value | Create the view |
| Later value | Call update(data); keep DOM, selection, scroll and zoom |
null value |
Passed to the view as data |
| In-place mutation | No update; send a new value |
| Values in one UI tick | May coalesce into the latest update |
| CSS hide or tab switch | Keep the view, including hidden host regions |
output.display: false |
Unmount and dispose the view |
JSEE also disposes a view when its renderer or descriptor is replaced, its output
is removed or cleared to undefined, a media output is blocked, or the app is
destroyed. dispose() runs once to release listeners, observers, timers and
graphics resources; JSEE clears the container. An update error retires the view
and displays an error in its card. A subsequent value can mount it again.
Hooks run synchronously on the UI thread. Renderers that start asynchronous work must cancel it or ignore late results when the source changes.
Registration and export
Registrations are copied at construction and belong to that instance. Different apps can register different views under the same name. The registry handles leaf outputs; groups keep their layout. Unknown names and invalid controllers show errors. Existing function outputs that return a cleanup function still work.
Save/Copy use the output’s declared type. Schema export retains renderer names,
but exportHTML() and download() reject host renderer registrations because
they cannot serialize host closures and dependencies. Bundle the host app and its
renderer imports, or remove output.renderer to use the built-in view.
Renderers are trusted host code; JSEE’s security options do not constrain arbitrary
renderer code. Registration is independent of the minimal/bulma Vue theme.
Persistent views example:
a selectable sensor table and chart retain selection and scale across worker runs.
TypeScript helpers OutputRenderer<T>, RendererView<T> and RendererRegistry
are exported from @jseeio/jsee/schema.
Display images with annotations
Use JSEE.imageView to display an image with boxes, points or colored masks over
it, for example, detection boxes or a segmentation mask supplied by your model.
Clicks report positions in the original image, so your app can use them as model
inputs. You can also zoom and pan the display without changing the image data.
The viewer draws annotations; it does not perform detection or segmentation.
Create a view with JSEE.imageView(container, initial, options).
Bundled hosts can import { imageView } from @jseeio/jsee/image-view. Register
it under an application name, or mount it directly in a host-owned region and
call dispose() when that region is removed. The same update/dispose lifecycle
works in both cases. The annotation example
uses worker results, a mask, a box and a point without ML dependencies.
const view = JSEE.imageView(container, {
source: imageFile, revision: 1,
boxes: [{ x: 10, y: 20, width: 100, height: 80, color: '#fff' }],
points: [{ x: 60, y: 60 }],
masks: [{ width: 1, height: 1, data: [255, 0, 0, 255],
extent: { x: 10, y: 20, width: 100, height: 80 }, opacity: 0.4 }]
}, { onSelect: ({ x, y, revision }) => console.log(x, y, revision) })
await view.ready
view.setViewport({ zoom: 2, panX: 10, panY: 0 })
view.update({ revision: 1, points: [{ x: 70, y: 60 }] })
Coordinates and selection
Annotation positions use pixels in the decoded/oriented source image. A box at
{x: 100, y: 50} starts 100 pixels from its left edge and 50 from its top edge,
regardless of the image’s displayed size or device pixel ratio.
Masks contain row-major RGBA bytes, grid width/height and an explicit extent in source pixels. They paint without
interpolation. JSEE does not infer scaling, decode tensors or normalize scores.
| Setting or method | Behavior |
|---|---|
fit |
Show the whole image (contain, default), crop to fill the viewer (cover), or stretch it (fill) |
mirror: true |
Flip the image horizontally, including annotations and click coordinates |
setViewport(patch) |
Zoom or pan the display using zoom, panX and panY; pan is in CSS pixels |
sourceToClient(point) |
Map source pixels to viewport CSS coordinates |
clientToSource(point) |
Map back; includes inside, without clamping letterbox clicks to image edges |
onSelect |
Report source coordinates and revision; does not run a model |
setSelection(pointOrNull) |
Change the marker without emitting another click |
getState() |
Read viewport and selection |
Mapping methods take a point {x, y} and return null until source pixels are
available. Mapping cannot recover precision lost to pointer-event rounding on a
scaled-down image. A host can pass a selection to setInputs and invoke the model
explicitly.
Source revisions and loading
| Update | Result |
|---|---|
| New source | Requires an increasing nonnegative integer revision; clears layers, selection, zoom and pan |
| Current revision | Replace supplied boxes, points or masks; omitted fields and viewport state remain unchanged |
| Empty layer array | Clear that layer |
| Older revision | Return false |
| Changed source at the same revision | Throw an error |
Sources can be URLs, Blobs/Files, bitmaps, canvases or image/video elements.
URL/Blob decoding is asynchronous: ready resolves true for an accepted load or
false for failure/supersession; onError receives load failures.
JSEE closes bitmaps it decodes on replacement/disposal, including late loads.
Caller-supplied bitmaps and media elements remain caller-owned. dispose() releases
observers, listeners and view references. This renderer does not change model
input/output types or the bitmap execution ownership rules above.
Overlaying an existing image or video
Set {overlay: true} and use the media element as source. JSEE paints annotations
and follows the element’s object-fit box and pixel/percentage object-position on
resize/load. The host owns playback, positioning and viewport transforms;
setViewport is unavailable. Match mirror to the host’s horizontal mirroring.
Arbitrary CSS rotations and calc() object-position are unsupported.
Pass {canvas} to reuse a host canvas. Disposal clears it and restores its style
instead of removing it. Give the container a height when a fixed display area is
needed.
Use overlay mode for playing video; owned-image mode does not continuously repaint video frames. Inference, heatmaps, annotation editing and a pan/zoom gesture UI are outside this API. The host renderer export restrictions also apply.
Named stages in the existing pipeline
Give stages IDs to connect model outputs to later inputs and rerun only affected
stages. JSEE expects a flat model array, input groups and namespaced outputs; a
host combining tool schemas must flatten them first. Unnamed pipelines keep their
existing merge/stop behavior.
const schema = {
inputs: [
{ type: 'group', name: 'gen1', elements: [{ name: 'n', type: 'int', default: 4 }] },
{ type: 'group', name: 'fit1', elements: [{ name: 'x', type: 'int', required: false }] }
],
model: [
{ id: 'gen1', scope: 'gen1', type: 'function', code: 'input => ({ count: input.n })' },
{ id: 'fit1', scope: 'fit1', type: 'function', code: 'input => ({ result: input.x * 2 })',
bindings: { x: { stage: 'gen1', path: ['count'] } } }
],
outputs: [{ name: 'fit1.result', type: 'number' }]
}
const app = new JSEE({ schema, container: '#app', runtimeURL: '/jsee.core.js' })
await app.ready
await app.invoke({ gen1: { n: 5 } }, { target: 'fit1' }) // { 'fit1.result': 10 }
Stage definitions
| Field | Meaning |
|---|---|
id |
Unique stage ID; required on every stage when using named stages |
scope |
Root input group supplying the stage’s values |
bindings |
Replace scoped values using {stage, path: ['output', 'nestedField']}, {input: ['publicInput']} or {value: literal} |
from |
Continue another stage’s internal model stack; records merge and stop propagates along that chain, not independent branches |
enabled: false |
Exclude the stage from full runs; dependents require it enabled |
render: true |
Run a browser view after its upstream models; mark public outputs required: false for headless use |
Cycles and missing stages fail before execution. Dynamic input updates stay within the stage’s declared input group. File bindings use the receiver’s text/raw/stream mode; saved projects need replayable stream sources, not live iterators.
Running and inspecting stages
| Call | Behavior |
|---|---|
Default Run or invoke() |
Reuse current stage results |
invoke(values, {target}) |
Execute the target’s required upstream stages and associated views |
invoke(values, {force: true}) |
Rerun all stages, or the selected target when supplied; the form also offers Rerun all |
getStages() |
Read stage status, revisions and results |
planPipeline(target) |
Read execution order |
getStageInputs(id) |
Read mapped values before input preparation |
stop() |
Cancel execution |
Input or upstream revision changes invalidate dependent stages.
invoke(undefined, {target, transient: true, overrides: {inspect: true}}) can run
an app-defined inspection operation without replacing retained trained results.
JSEE does not interpret the meaning of inspect.
Saving state and exporting an app
| Method | Contents |
|---|---|
getSchema() |
Descriptor with live defaults |
getPipelineState() |
Stage results with SHA-256 definition fingerprints, separate from source code |
restorePipelineState(state) |
Restore those saved results |
exportSchema() |
Resolved code URLs, loaded bytes pinned with sha256, and JSON-safe file data |
exportHTML({runtime}) |
JavaScript assets deduplicated by content hash in the offline DOM cache, plus the supplied runtime |
Named-pipeline download() |
HTML export using runtimeURL |
Restore compares file bytes once; subsequent edits use immutable file identity. Render functions are recreated rather than persisted, and function outputs may return cleanup functions. Hosts own storage and authoring metadata.
HTML export supports JavaScript function models and declared assets. Python, ES-module models and undeclared network dependencies are unsupported. An exported app reruns the computation; it is not a saved result snapshot. Random sources need seeds for repeatable results.
Running named stages outside the browser
Node CLI, HTTP /run and MCP execute the same flat schema, skipping render stages.
Remote JavaScript references require SHA-256 pins: model/import bytes are verified
before execution, and security.allowNetwork: false blocks fetching. Native Python
does not implement named stages. See test/fixtures/composition.json and
test/mcp/composition.test.js.
Host-owned regions
Choose which UI the host supplies:
| Constructor option | Effect |
|---|---|
controls: false |
Omit JSEE’s Run/Stop/Reset footer |
toasts: false |
Suppress this instance’s toast messages, including loading/errors |
regions: {inputs, outputs} |
Mount those sections in host elements |
regions: {inputGroups: '#inspector'} |
Place selected input groups in a shared inspector |
controls and toasts default to true. Errors still reject the invocation and
emit error; the host must display them. These settings do not affect browser
notifications (schema.notify) or execution permissions.
Mounting a host editor in an input group
To keep JSEE’s normal form and Run/Stop, mount an optional host editor inside an input group instead of moving the form out of the app:
await tool.ready
const target = tool.getInputGroupContainer('fit1')
// Mount or Teleport your editor into target; publish values with tool.setInputs().
// Unmount the host editor before tool.destroy().
Input groups accept direction: 'column' to stack fields vertically; the default
remains a row. Scoped model input updates recurse through declared groups, for
example {diff1: {column: {options: ['', 'price'], value: 'price'}}}.
Updates cannot create undeclared fields or modify another stage’s scope.
In the normal form, selectInputGroup(name) finds nested groups and expands
the selected section and its ancestors/children, collapsing unrelated sections.
Selection changes presentation without executing or remounting models.
getInputGroupContainer(name) returns the group’s editor element, or null when
it is not rendered. The target lives until the group or app is removed and follows
accordion visibility. The host retains its own framework context; JSEE does not
load the editor’s framework. Editors are authoring UI and are not serialized into
schema/HTML exports; their settings remain ordinary input values.
Placing controls and outputs in host elements
For a shared inspector use regions: {inputGroups: '#inspector'} and call
selectInputGroup('fit1') after ready. It changes field visibility without
rerunning or disposing stages. Use controls: false when the host owns Run/Stop.
const tool = new JSEE({
schema,
container: '#tool-root',
regions: { inputs: '#tool-controls', outputs: '#tool-results' }
})
await tool.ready
const unsubscribe = tool.on('result', outputs => console.log(outputs))
const outputs = await tool.invoke({ x: 4 })
// Later: unsubscribe(); tool.destroy()
Create connected, instance-specific region elements outside container before
construction, and retain them until destroy(). Either region is optional.
JSEE uses its own Vue root and Teleport; host Vue/Vuetify providers are not inherited.
Each region carries JSEE’s theme variables from schema.design. Hide the host’s
region elements to switch views without rerunning or disposing the tool. Destroy
removes teleported content and listeners. Run/Stop controls remain with inputs.
Host events and invocation
| Event | Payload |
|---|---|
inputs |
Current values after Vue applies a UI edit, programmatic patch, reset or model-driven input update |
start |
Input snapshot for the run |
result |
Values returned by invoke |
error, cancel |
Error object |
Reactive schemas retain their automatic-run behavior;
use reactive: false and no reactive input flags for manual workspace tools.
Plain objects/arrays are copied; opaque runtime values retain their identities.
invoke() never queues behind another run. Existing run() queue/interval
behavior remains available to standalone applications.
JSEE is part of the StatSim ecosystem.
JSON commands and canonical HTTP
These commands run the whole pipeline and return declared outputs in a consistent
JSON response. Here, canonical HTTP means the shared /run endpoint; the
older per-model endpoints are documented under legacy HTTP.
Both packages provide a command named jsee: the npm command executes JavaScript models; the Python command executes Python models. If both are installed, use npx @jseeio/jsee for Node and the full path to your Python environment’s bin/jsee for Python. Existing flag-based commands remain supported.
The Node and Python CLIs accept the same descriptor commands. Use a JS model descriptor with Node and a Python model descriptor with Python:
jsee inspect schema.json --json
jsee validate schema.json
jsee run schema.json --input input.json --json
jsee serve schema.json --port 3000
Inspection and execution
inspect reports input/output JSON Schemas and runtime capabilities without importing models. validate checks computation structure, names, known field types, numeric constraints and defaults. It requires a model; render-only descriptors remain valid for the browser. Neither command tests model availability, executes custom browser validation expressions, or validates every UI extension. The full authoring vocabulary remains available in schema.json.
run reads UTF-8 JSON from stdin by default (--input -), or from the selected file. It writes one JSON envelope to stdout; model diagnostics go to stderr. Successful calls return {runId, status: "succeeded", outputs, artifacts}. Failed calls return {runId, status: "failed", error: {code, message}}. Exit codes are 0 for success, 2 for descriptor/JSON-request errors, and 1 for invocation or output failures.
Output validation and files
Results include only declared outputs, with group names flattened and aliases resolved. Accumulated inputs and undeclared intermediate values are omitted.
- Missing outputs or non-JSON values fail with
JSEE_OUTPUT_INVALID. Errors identify the path and value kind, such asinfo.a: undefined is not a JSON value. NaN,Infinityand-Infinityfail at any depth; they are never silently replaced bynull.- Before storing files, CLI/HTTP/MCP validate numeric, string and boolean outputs, table containers, media fields and optional object
valueSchemarules. Chart semantics and unsupported JSON Schema keywords are not checked. - Without
valueSchema,objectaccepts any JSON value. Declare bytes astype: "file".
| File result | Representation |
|---|---|
| Model return value | Text or bytes, directly or under content, value or data; optional filename and mime |
Value in outputs |
{artifact: "<runId>/<index>"} |
Entry in artifacts |
id, output, name, mime, size in bytes, sha256, uri |
| CLI storage | .jsee-artifacts/ in the working directory, or --artifact-dir; files have file: URIs |
Model-provided paths and URLs are never opened or fetched as file content.
HTTP routes and limits
Canonical serving adds these routes to the existing UI and per-stage endpoints:
| Route | Result |
|---|---|
GET /schema |
Computation descriptor retaining local model references |
GET /openapi.json |
Whole-pipeline request, response and artifact descriptions |
POST /run |
Execute the entire pipeline with a JSON input object |
POST /__jsee/artifacts?name=data.csv |
Upload raw bytes; Content-Type declares MIME; returns {artifact, metadata} |
GET /__jsee/artifacts/<runId>/<index> |
Download the referenced file as an attachment |
A stage named run conflicts with canonical /run and is rejected before loading. Rename that stage or retain the legacy serving command. Existing jsee schema.json --run, --serve, and -o index.html -f forms keep their behavior. Python also accepts serve(schema, canonical=True) and create_app(schema, canonical=True) with a descriptor path or dict referencing local model files; call the WSGI application’s close() when retiring it.
| Limit | Default | Option |
|---|---|---|
| JSON request or raw HTTP upload | 50 MiB | --max-body-bytes |
| Stored artifacts, including pending uploads | 64 MiB | --max-artifact-bytes |
| Stored file count | 1,024 | Fixed |
A batch that exceeds the quota publishes no partial artifacts. Input and output files share the store: a transform retaining both may need a larger quota. These limits do not bound model memory or execution time.
HTTP artifacts live in a private temporary directory until shutdown. Their URIs are relative to the server and do not survive a restart; a killed process may leave temporary files for host cleanup. Downloads verify size/SHA256 and return 409 if stored bytes changed.
Passing files between runs
Ordinary type: "file" inputs accept inline text or an existing { "artifact": "<runId>/<index>" } handle. References resolve in the current store before model execution: ordinary files become strict UTF-8 text, while arrayBuffer: true becomes ArrayBuffer in JS or bytes in native Python. Legacy inline numeric arrays keep their existing meaning. Raw/stream file modes retain their separate semantics and do not accept artifact references.
An output reference can be passed unchanged to the next run. The ID is scoped to the store; SHA256 is integrity metadata, not an access key. Invalid, foreign or unavailable IDs, changed file bytes and MIME mismatches fail as invalid inputs. Strings are never implicitly opened as paths or URLs. In-process invokeSchema(..., {artifactStore}) and invoke_schema(..., artifact_store=...) can use the existing transport module’s ArtifactStore; calls without a store reject references.
# Both Node and Python CLIs; output is {artifact, metadata}.
jsee artifact-import sales.csv --mime text/csv --artifact-dir .jsee-artifacts
# Put {"csv":{"artifact":"<returned ID>"},"missing":"drop"} in input.json.
jsee run schema.json --input input.json --artifact-dir .jsee-artifacts
CLI registrations persist alongside bytes, so subsequent invocations can reuse handles. Registrations created before this feature cannot be recovered from old bare files. Writers serialize publication with .write-lock; after an interrupted CLI write, verify that no writer remains before removing that lock. HTTP registrations last until host shutdown; MCP registrations last for the session. Moving a file to another host/session requires explicit download/upload. The JSON request and raw HTTP upload limits are both controlled by --max-body-bytes; ingestion does not remove store quotas.
HTTP errors and access
/run accepts JSON only.
| Status | Cause |
|---|---|
| 400 | Invalid inputs |
| 413 | Request body too large |
| 415 | Unsupported content type |
| 422 | Unsupported execution capability |
| 500 | Model or output error |
| 507 | Artifact quota exceeded |
Servers bind to localhost by default. Canonical serving applies same-origin access to every route, including legacy endpoints and static files. Legacy serving retains wildcard CORS until a host policy is configured. See authentication and CORS for host-wide settings.
Security flags gate JSEE-managed operations; they do not provide an OS sandbox.
Remote MCP, streaming/cancellation through /run, and multi-descriptor serving
remain unsupported.
HTTP authentication and CORS
The same host policy covers the UI, runtime/model files, discovery, canonical and legacy API routes, streams, artifacts, and error responses. Configure it with either CLI:
# Read an existing shared token from APP_TOKEN; its value stays out of argv.
jsee serve schema.json --auth-token-env APP_TOKEN
jsee serve schema.json --auth-token-env APP_TOKEN --cors-origin https://client.example
Alternatively use --auth-token TOKEN. The options also work with legacy serving commands. Python serve() and create_app() accept auth_token, auth_token_env, and cors_origins (a list). Token configuration is validated before importing descriptor models; missing/empty environment values fail startup. Auth/CORS flags are server-only and cannot become schema defaults, model inputs or verbose argv output. They are rejected in MCP stdio and JSON command modes.
Client authentication
Clients send Authorization: Bearer TOKEN. Browsers use the built-in HTTP login prompt: username jsee, password the same token. Both authenticate every actual request; there are no public UI, asset or discovery bypasses. Tokens are never embedded in generated HTML or schemas. Missing/invalid credentials return JSON 401 with authentication challenges. Authenticated responses use Cache-Control: no-store, including streams and downloads. OpenAPI declares Bearer and Basic as alternatives.
Allowed origins
| Setting | Origin policy |
|---|---|
jsee serve schema.json |
Same-origin browser requests and requests without Origin; external origins return 403 |
| Legacy serving, no policy options | Wildcard CORS retained for compatibility, now applied consistently to all routes |
| Any authentication option | Same-origin by default, including legacy serving |
--cors-origin https://client.example |
Also allow that exact origin; repeat for more origins |
--cors-origin none / Python cors_origins=[] |
Same-origin only |
--cors-origin '*' |
Public cross-origin access; incompatible with authentication |
Origins must use their serialized HTTP(S) form, without paths, trailing slashes, user info or explicit default ports. Authenticated/explicit-origin policies reject opaque null origins. Requests without Origin still require authentication when configured. CORS controls browser access; it does not authenticate callers.
Allowed preflights return 204 without credentials and permit Authorization/Content-Type for GET, HEAD, POST and OPTIONS. The actual request still needs credentials. Disallowed origins, requested methods or headers fail before routing/body parsing. CORS headers also cover authorized-origin error responses; Vary: Origin prevents mixing origins in caches. File attachment names are exposed through Content-Disposition.
Proxies and deployment
Serve credentials over HTTPS through a trusted TLS proxy, or use an SSH tunnel. For a TLS proxy, explicitly allow its public HTTPS origin; JSEE does not trust forwarded headers. Runtime URLs in served Node pages are relative, so they retain the browser’s host, scheme and authentication. This is shared-token access control, with no user accounts, OAuth, per-tool permissions or execution sandbox. See the Bearer, Basic and CORS standards.
MCP over stdio
Start one descriptor as an MCP tool with jsee serve schema.json --mcp-only. This mode uses stdin/stdout for MCP and stderr for model diagnostics; it opens no HTTP listener. Node requires Node.js 20+ and uses the official @modelcontextprotocol/server SDK (2.0.0). Native Python requires Python 3.10+ and the optional server extra (mcp 2.2.0); install with python -m pip install 'jsee[server]==0.9.0'. Python’s base installation remains stdlib-only.
Configure an MCP client to launch the installed jsee executable with arguments ['serve', '/absolute/path/schema.json', '--mcp-only']. Use the executable belonging to the descriptor’s language. For a Node checkout, use command node and prepend /absolute/path/jsee/bin/jsee to those arguments.
Tool discovery and results
tools/list derives a computation tool from the descriptor’s stable id, title, description and input schema without importing model code. tools/call executes the same whole-pipeline invocation as JSON CLI/HTTP. Success returns the complete {runId, status, outputs, artifacts} envelope as structuredContent and JSON text, with its generated outputSchema. Invalid inputs, execution/output failures and artifact quota failures return isError: true and the failed envelope as text. Unknown tools or artifact URIs produce protocol errors.
Reading MCP artifacts
File results include resource_link entries with session-scoped jsee-artifact: URIs. resources/list exposes metadata; resources/read returns exact bytes as a base64 blob only when requested. Verify the decoded bytes against size and sha256 in the run envelope. Store limits remain 64 MiB/1,024 files per session (--max-artifact-bytes changes bytes); rejected batches expose no files. Files disappear on orderly close or SIGTERM and cannot be retrieved by another session. SIGKILL or a crash can leave temporary files.
--max-body-bytes bounds decoded arguments (50 MiB by default); Node also applies that limit to its incoming wire buffer, including protocol overhead. Python’s SDK reads a message before JSEE checks its arguments. These limits do not bound model memory. A resource read returns the entire file: base64 adds overhead, and the client’s message limit may be lower than the store quota. Large-file chunking is not implemented.
Uploading MCP artifacts
Discovery also lists upload_artifact, renamed jsee_upload_artifact if the
computation already uses that ID.
- Start with
{data: "<base64>", name: "data.csv", mime: "text/csv", offset: 0}. - Continue with the returned
uploadID andoffset. Each chunk can contain at most 262,144 decoded bytes. - Set
final: trueto publish{artifact, metadata}. Pass{artifact: result.artifact}to a compatible model input. - To abandon an incomplete upload, send
{upload: id, cancel: true}.
Up to 16 incomplete uploads share the store byte quota and remain absent from resource listings. The client transfers the chunks; model arguments contain only the artifact reference. Client/server message limits must accommodate base64 encoding and protocol overhead.
MCP support and limits
Independent Node and Python SDK clients test both servers in legacy negotiation and protocol 2026-07-28 mode, including the clean sales workflow. MCP calls use the existing programmatic invocation lifecycle; persistent model state across calls is not a portable contract. This adapter supports terminal calls for one descriptor. Remote MCP, streams and forced cancellation remain pending; HTTP authentication is separate from stdio process access. Model source is trusted code; stdio separation is not a sandbox.
CLI: Node.js
jsee [schema.json|package] [data...] [options]
jsee init [template] [--html]
Commands
jsee init [template]
Scaffold a new project. Templates: minimal (default), chat.
jsee init # schema.json + model.js + README.md
jsee init chat # chat template
jsee init --html # single index.html with CDN script
jsee <schema|package> [data...]
Start a dev server or generate a static HTML file from a local schema or a JSEE app package. Package inputs resolve the package jsee field, for example { "jsee": "schema.json" }, and use the package directory as the app root. If the app package is not installed locally, let npm provide both packages with npx -p @jseeio/jsee -p <package> jsee <package> --serve.
jsee schema.json # dev server on localhost:3000
jsee schema.json --host 0.0.0.0 # expose dev server on the local network
jsee @statsim/gen --serve # serve a resolvable JSEE app package
jsee schema.json -o app.html # generate static HTML
jsee @statsim/gen -o app.html --bundle # bundled HTML from package app
jsee @statsim/gen --run --dataset Moons --nSamples 500
jsee @statsim/gen --run --dataset Moons --file moons.csv
npx -p @jseeio/jsee -p @statsim/gen jsee @statsim/gen --serve
JSEE app packages can also expose their own bin with require('@jseeio/jsee').runPackage(__dirname, process.argv.slice(2)). The helper reads the package jsee field, uses the package root as the app root, and forwards ordinary JSEE CLI options.
Use --run for one-shot CLI execution. With no output target, JSEE writes a pipeable stdout stream: a single file output writes its file content, a single text/table/object output writes that value, and multiple outputs write a JSON object. In --run mode, --outputs <dir> writes all outputs into a directory, while an output-name flag such as --file moons.csv or --output-file moons.csv writes one named output to a specific path.
Options
| Flag | Description |
|---|---|
-i, --inputs <file> |
Input schema JSON file (default: schema.json) |
-o, --outputs <file> |
Output file path(s), comma-separated (HTML, JSON) |
-d, --description <file> |
Markdown file to include as app description |
--host <addr> |
Dev server host (default: 127.0.0.1; use 0.0.0.0 to expose on the local network) |
-p, --port <number> |
Dev server port (default: 3000) |
-v, --version <version> |
JSEE runtime version (latest, dev, or semver) |
-b, --bundle[=mode] |
Embed runtime and declared dependencies into HTML (default, hybrid, pyodide, pyodide-single, or offline-strict) |
-f, --fetch |
Backward-compatible alias for --bundle |
-s, --serve |
Serve explicitly (default when no output is provided) |
--run |
Execute the model once from the CLI and write pipeable outputs |
-e, --execute |
Run models server-side (auto-enabled when serving local .js models) |
--client |
Force client-side execution (disable auto server-side) |
-c, --cdn <url\|bool> |
Rewrite model URLs for CDN deployment |
-r, --runtime <mode> |
Runtime source: auto\|local\|cdn\|inline or a custom URL/path |
--verbose |
Enable verbose logging |
--help, -h |
Show usage info |
--bundle
Embeds declared browser assets into HTML: the JSEE runtime, model/view/render code, and all imports are stored in hidden base64 <script data-src="..." data-encoding="base64"> elements. Local files are detected by checking the filesystem (so bare paths like dist/core.js work alongside ./relative.js); anything not found locally is fetched from CDN.
When security.allowEval is false, hidden bundled JavaScript model/import code is intentionally blocked at runtime. Use a separate type: "module" model URL for the no-eval JavaScript path; set worker: true if that module should run in JSEE’s Web Worker.
Model dependencies and schema imports are separate mechanisms: local model files that use require() or static import/export are bundled with optional esbuild during --bundle, so npm dependencies can become part of the generated HTML; schema imports remain explicit browser assets loaded before the model, such as CDN globals, local helper scripts, CSS files, Plot/Three/Leaflet, or other side-effect libraries.
For browser Python, --bundle caches a configured model.pyodide.url loader script when model.pyodide is explicit. Pyodide’s WASM, stdlib zip, and package wheels are still loaded from model.pyodide.indexURL, so strict no-network Pyodide bundles require those Pyodide release assets. Use --bundle=pyodide to copy a local Pyodide asset directory, or fetch an explicit remote Pyodide indexURL, beside the generated HTML and rewrite the generated schema to the copied relative path. Use --bundle=pyodide-single to embed Pyodide core assets and declared package wheels into the HTML itself. Declare package wheels to fetch with model.pyodide.packages or Python-model imports; auto-detected Python imports are not bundled.
This does not discover arbitrary fetches, model weights, host closures or dynamically loaded dependencies. API models still need their server. Verify the generated app with networking disabled before distributing it as offline.
Bundle modes:
--bundleor--bundle=hybridkeeps the compatibility behavior: JSEE inlines what it can and warns when configured Pyodide assets still point at a remoteindexURL--bundle=pyodiderequires explicitmodel.pyodideassets, copies a localindexURLdirectory or fetches a remoteindexURLcore asset set beside each generated HTML output, fetches declared package wheels frompyodide-lock.json, and caches the copiedpyodide.jsloader in hidden storage--bundle=pyodide-singlerequires explicitmodel.pyodideassets, embeds Pyodide core assets (pyodide.asm.wasm, stdlib zip, lock file, asm JS/module when present) plus declared package wheels into a hidden manifest, and rewrites generated Pyodide URLs to an in-pagejsee-pyodide://asset shim--bundle=offline-strictfails generation if the bundle would still need runtime network for API models, remote model URLs, implicit default Pyodide, remote PyodideindexURL, or a missing local Pyodide asset directory relative to the generated HTML file
--runtime
Select the runtime source for generated HTML:
auto(default):inlinewhen--bundle, otherwisecdnfor file output andlocalfor dev serverlocal:/dist/...for served pages; generated files use the configured server host and portcdn: jsdelivr CDN URLinline: embed runtime code directly in HTML- Any other value is used as a custom
<script src="...">path/URL
Data inputs
Positional arguments after the schema file and named --key=value arguments are mapped to schema inputs. Values are auto-detected:
| Value | Detected as |
|---|---|
42, 3.14 |
Number |
'[1,2,3]', '{"a":1}' |
JSON (array or object) |
data.csv (existing file) |
File path |
hello |
String |
Inputs explicitly set from the CLI are locked (non-editable) in the GUI and used as defaults for server-side execution. CLI options and their aliases are reserved: --port/-p configure the server even if the schema has an input named port or p. To override a colliding input such as b, pass positional values (jsee schema.json 3 4 for inputs a, b), declare a non-conflicting input alias, or use JSON with jsee run. CLI option defaults never change model defaults. Unnamed groups use their child fields in positional order.
# Positional — mapped to inputs in schema order
jsee schema.json 42 hello
# Named — matched by input name
jsee schema.json --x=100 --y=200
# File path
jsee schema.json data.csv
# Mixed
jsee schema.json data.csv --format=json
Server-side execution
When serving (no -o flag), JSEE automatically enables server-side execution if all models point to local .js files. Use --client to force browser execution.
jsee schema.json # auto server-side (local .js models)
jsee schema.json --client # force browser execution
jsee schema.json -e -p 3000 # explicit server-side on port 3000
File and folder serving
JSEE can serve individual files or entire folders without a schema or model:
jsee report.pdf # serve PDF with viewer
jsee photo.png # serve image
jsee data.csv # serve CSV as table
jsee data/ # serve folder with file browser
For single files, JSEE auto-detects the output type from the file extension (image, pdf, audio, video, table, markdown, code). For folders, JSEE creates a file browser with selection checkboxes, stats summary, and a preview of the selected file.
The dev server file endpoints are app-root contained. /__jsee/file and /__jsee/folder only serve files under the schema/package root, or under the explicit file/folder path passed to identity serving (jsee data/, jsee report.pdf). Python schema static files are also contained to the schema root. Path traversal, symlinks that resolve outside the allowed root, dotfiles, and node_modules paths are denied.
Output type auto-detection also works for model results: if a model returns a string ending in .png, it renders as an image automatically.
Serve bar
When serving, a top bar appears with the server address and, when server-side execution is active, a Browser/Server toggle to switch execution mode. Python-served pages also include a Save HTML button. Bundled generated output files show a Download HTML button instead of the serve bar.
Whole-pipeline invocation
Call a pipeline directly from Node or Python without starting a server. This API returns native values and intermediate results. Use the JSON CLI or HTTP API when you need declared-output filtering, serialization and stored file artifacts.
| Contract | Browser | Node | Native Python |
|---|---|---|---|
| Groups, defaults, options, numeric constraints | Validated | Validated | Validated |
| Custom input expressions and nonempty controls | UI validation | Not evaluated | Not evaluated |
| Pipeline execution | Browser orchestrates stages | invokeSchema, JSON CLI, canonical HTTP and MCP execute JS pipelines; legacy stages remain |
invoke_schema, JSON CLI, canonical HTTP/WSGI and MCP execute Python pipelines; legacy stages remain |
| Python callable selection | Pyodide entry |
Not a native Python runtime | entry, with a separate endpoint name |
| Interruption | Worker termination; cooperative main-thread and fetch cancellation | No OS containment | Stream cleanup; no forced interruption of arbitrary model code |
Identified stages are supported in browser/Node execution; native Python supports ordinary sequential pipelines.
Node exports invokeSchema(schema, inputs, { schemaPath, cwd, log }) from @jseeio/jsee/cli. Python exports async invoke_schema(schema, inputs, cwd='.') from jsee. Both validate inputs before importing models, apply declared defaults, and execute the complete model pipeline. See the clean sales example for JavaScript, Python, and a generated offline form with a CSV download.
The returned envelope is { runId, status: 'succeeded', result } or { runId, status: 'failed', error: { code, message } }. Each call gets a new run ID; model fields named status or runId remain inside result. Error codes are JSEE_SCHEMA_INVALID, JSEE_INPUT_INVALID, JSEE_CAPABILITY_UNSUPPORTED, and JSEE_EXECUTION_FAILED. These checks cover invocation prerequisites and existing input-value rules; full descriptor authoring validation remains separate.
Results retain accumulated pipeline data, including intermediate values. Only records merge; arrays, scalars and null replace the previous value. Python None corresponds to JS null; JavaScript’s undefined retains the previous value. The existing truthy stop field stops later stages. JSON input containers are copied before execution. Runtime values are returned directly, without HTTP serialization or file persistence.
This interface supports local JS functions/classes/async initializers and native Python sync/async functions in files. Python uses keyword binding by default and schema input order for container: 'args'. Python stages retain the loader’s unique-name requirement. Ordinary schema imports, unpinned remote models and streaming are rejected;
normal imports inside model source remain usable. Node also supports remote JS
model URLs and per-model imports with SHA-256 pins, verifying bytes before execution.
Native Python requires local model files. Calls execute on the caller’s thread without forced interruption, worker isolation, or enforcement of browser timeout/security flags.
Legacy HTTP endpoints
Every server (both Node.js with --execute and Python) exposes these endpoints:
| Route | Method | Description |
|---|---|---|
/ |
GET | Interactive GUI |
/api |
GET | Schema and endpoint discovery |
/api/openapi.json |
GET | Auto-generated OpenAPI 3.1 spec |
/{modelName} |
POST | Execute model with JSON or multipart input |
Both servers accept application/json and multipart/form-data POST bodies and
return JSON errors for invalid requests. Legacy serving defaults to wildcard CORS;
canonical serving and explicit authentication/origin settings use the
host access policy. The Node dev server accepts JSON and multipart request bodies up to 50 MiB. Python serve() and create_app() share request decoding and default to the same 50 MiB limit; set a positive max_body_bytes or the Python CLI’s --max-body-bytes to change it. Invalid lengths or input values return 400; declared oversized bodies return 413 before reading the body. This size limit does not provide execution isolation or a request deadline. Python serve() runs with a threaded dev server, awaits async model functions, and streams sync or async generator functions as SSE when stream=True.
The OpenAPI spec is generated from the same schema contract: request bodies include JSON inputs and multipart file uploads, responses include declared output properties (including outputs nested in groups), streaming models advertise text/event-stream, schema.examples become request examples, and standard JSON error responses are documented. This is descriptive only; the runtime response payload remains backward-compatible.
# Run the model via API (JSON)
curl -X POST http://localhost:3000/modelName \
-H 'Content-Type: application/json' \
-d '{"x": 3, "y": 4}'
# Run with file upload (multipart)
curl -X POST http://localhost:3000/modelName \
-F 'text=hello' \
-F 'file=@image.png'
# Get OpenAPI spec
curl http://localhost:3000/api/openapi.json
Return value serialization
Both servers normalize model return values consistently:
| Return type | JSON response |
|---|---|
| Object/dict | returned as-is |
| Primitive (number, string, bool) | {"result": value} |
| Tuple/list (Python) | {"result": [a, b]} |
| Buffer/bytes | MIME-sniffed data URL, e.g. {"result": "data:application/pdf;base64,..."} |
| PIL Image (Python) | {"result": "data:image/png;base64,..."} |
| list[dict] (Python) | {"result": {"columns": [...], "rows": [...]}} |
Python
Python: includes the browser runtime; no Node.js required:
python -m pip install jsee
Requires Python 3.9+. For Python MCP support, use Python 3.10+ and python -m pip install 'jsee[server]'. Models may need additional packages.
See the Python quick start for a complete example and the Python README for type annotations, schema generation, CLI commands, streaming and WSGI deployment.
Status and limitations
- Browser regression tests run in Chromium. Firefox and WebKit have not been verified. Strict Python/WASM CSP deployments need separate verification; see the worker/CSP reference.
- Node and Python JSON CLI, HTTP
/runand MCP calls execute local models and return a completed result. Streaming is unsupported on that path. Node accepts SHA-256-pinned remote JavaScript models and per-model imports; native Python requires local model files. Browser execution and legacy streaming routes have different capabilities; see whole-pipeline invocation. - Identified pipeline stages are supported in browser/Node execution, not native Python. Named stages are optional; ordinary sequential pipelines remain available.
- MCP supports one descriptor over stdio. Remote MCP, canonical streaming and forced server-side cancellation are pending.
- Worker isolation keeps computation off the UI thread. Browser security flags gate JSEE operations; server execution runs trusted code with the process’s permissions. Neither is an OS sandbox.
- Offline use depends on the model’s assets and dependencies. API models need their server; Python bundles need Pyodide assets and declared packages. See bundle modes.
Versioning
JavaScript and Python share a major/minor compatibility line; compatible fixes can have different patch versions. schemaVersion: 1 versions the descriptor format separately. Omitting it retains version-1 behavior. Package version agreement does not remove runtime-specific limits.
Development
Build from source
From a checkout of https://github.com/jseeio/jsee:
npm install
npx playwright install --with-deps chromium
npm run build
python -m pip install './py[server]'
python -m pip install pytest build wheel
Use Node.js 20+ and Python 3.10+ for the complete suite (the base Python package supports 3.9+). The Playwright install step may require permission to install system packages. npm run build-dev is a faster development build; it does not qualify production CSP behavior or run the tests.
The maintained Python source is py/. Production builds copy the browser runtime and schema into its package data; installing a prepared wheel requires no Node.js.
Repository verification
npm run build builds browser assets, copies them into the maintained Python package, and runs types, unit tests, and the standard Playwright suite. npm run test:python-native checks the native runtime and builds/installs packages outside the checkout. After installing ./py[server], run npm run test:mcp for independent SDK client/server checks. Set JSEE_MCP_PYTHON to use a separate Python executable.
The additional app suites exercise real computation:
| Command | Coverage |
|---|---|
npm run test:python |
Pinned Pyodide/NumPy, repeated entry calls, addition and CSV summary on the main thread and in workers |
python -m pip install -r test/python/requirements.txt then npm run test:python-apps-native |
Native pandas summary against the same CSV/result fixtures as Pyodide |
npm run test:ml |
GPT-2 text generation and camera -> CLIP worker -> view, with pinned weights and dependencies |
The app suites verify assets against test/fixtures/runtime-assets.json, downloading missing files into ignored tmp/test-assets/. Python assets use about 55 MB; ML assets use about 1.12 GB. These are download sizes, not runtime memory requirements. Browser requests use local assets; ML tests redirect the original apps’ known external URLs to the pinned copies and block other external requests. GPT-2 is rebuilt from its reviewed source and lockfile with dependency installation scripts disabled. The tests use genuine model inference and a deterministic simulated camera frame.
test/fixtures/real-apps/manifest.json records source hashes, origins, and intentional repairs to historical examples. Generated bundles, model weights, and dependency trees are excluded from Git and package contents. The default unit suite requires neither those dependencies nor model downloads. Browser Python tests pin Pyodide 314.0.7, NumPy 2.4.6 and pandas 3.0.2; a separate 0.24.1 loader regression covers the classic compatibility path. Single-file CLI and browser re-download tests run with network access blocked. For Python, browser download requires an app generated with --bundle=pyodide-single; it preserves those embedded assets instead of discovering arbitrary packages used during execution. This does not establish support for every runtime or Python package.
For focused compatibility checks, install the corresponding Playwright browser and dependencies, then set JSEE_TEST_BROWSER=firefox or JSEE_TEST_BROWSER=webkit. Chromium is the default. Set JSEE_TEST_NO_GPU=1 to disable GPU use in the Playwright suites; do not run test:ml as an interface-only check.
Help
Report problems with the descriptor, runtime version and reproducer in GitHub issues. JSEE is MIT licensed.
Changelog
See the changelog for release changes.