JSON-RPC Server Mode

Scriptling can run as a JSON-RPC 2.0 server over stdin/stdout or over HTTP, letting host processes (editors, daemons, tool runners) invoke Scriptling handlers as JSON-RPC methods.

Starting the Server

Use the --json-rpc flag by itself to start a stdio JSON-RPC server:

scriptling --json-rpc setup.py

The setup script runs once at startup and registers method and notification handlers via scriptling.runtime.jsonrpc. The server then reads newline- delimited JSON-RPC 2.0 requests from stdin and writes one response per line to stdout.

Keeping the setup script alive: As with the HTTP server, the setup script can call runtime.start_server() to stay alive alongside the stdio server instead of exiting after registration: useful for sharing state or running background work while serving requests.

Logging goes to stderr. In --json-rpc mode, all log output is automatically redirected to stderr so it never corrupts the JSON-RPC stream on stdout. You can safely combine --log-level debug with piping requests on stdin.

Use --json-rpc with --server to mount the same handlers over HTTP:

scriptling --server :8000 --json-rpc setup.py

HTTP JSON-RPC is served at POST /json-rpc. It can run alongside regular runtime.http routes and MCP tools:

scriptling --server :8000 --json-rpc --mcp-tools ./tools setup.py

Server Options

Flag Environment Variable Description Default
--json-rpc SCRIPTLING_JSONRPC Enable JSON-RPC server mode: stdio by default, HTTP /json-rpc with --server false
--server SCRIPTLING_SERVER HTTP server address for HTTP JSON-RPC (disabled)
--allowed-paths SCRIPTLING_ALLOWED_PATHS Allowed filesystem paths (none)
--kv-storage SCRIPTLING_KV_STORAGE Directory for persistent KV store in-memory
--libpath SCRIPTLING_LIBPATH Extra library search directories none
--package - Load libraries from a package (zip) none
--plugin-dir SCRIPTLING_PLUGIN_DIR Load plugin libraries none
--json-rpc-max-batch SCRIPTLING_JSONRPC_MAX_BATCH Maximum elements in one batch (negative = unlimited) 1000
--json-rpc-concurrency SCRIPTLING_JSONRPC_CONCURRENCY Maximum handlers running at once (negative = unlimited) 4 × CPUs
--json-rpc-cors-origin SCRIPTLING_JSONRPC_CORS_ORIGIN Browser origin allowed to call /json-rpc cross-origin (repeatable, * for any) same-origin

--json-rpc selects one transport. Without --server it uses stdio; with --server it mounts HTTP JSON-RPC at /json-rpc.

Security: Use --disable-lib subprocess to remove shell access. --allowed-paths limits participating library filesystem APIs; it does not constrain enabled subprocesses or host-side loading.

Over HTTP, the script middleware registered with runtime.http.middleware(...) guards /json-rpc the same as HTTP routes — use it for token or per-user key authentication (see Per-User Keys). Without a middleware, a --bearer-token guards the endpoint instead.

HTTP /json-rpc is same-origin by default. A browser request from another origin is refused with 403 before any middleware or handler runs, so a page on another site cannot make a visitor’s browser call your methods (a text/plain POST needs no preflight, so CORS headers alone would not stop the call). Clients that send no Origin header — curl, other servers, scriptling plugin clients — are unaffected. To serve a browser app on another origin, allowlist it with --json-rpc-cors-origin https://app.example.com (repeatable, or * for any): its preflight is answered and its responses carry Access-Control-Allow-Origin. The list replaces the same-origin default, so if pages the server serves itself also call /json-rpc, list the server’s own origin too. With --bearer-token, the preflight is answered without the token, as browsers never send it there; the request itself still needs it.

Registering Handlers

Handlers are referenced by string ("library.function"), the same model used by runtime.http; the module part may be a dotted path into a subdirectory ("routes.me.echo"). Each request runs on a fresh, isolated evaluator, so handlers are fully concurrent and a slow handler never blocks the next request.

# setup.py
import scriptling.runtime as runtime

runtime.jsonrpc.method("echo", "handlers.echo")
runtime.jsonrpc.method("divide", "handlers.divide")
runtime.jsonrpc.notification("progress", "handlers.on_progress")
# handlers.py
import scriptling.runtime as runtime

def echo(params):
    return params

def divide(params):
    if params["b"] == 0:
        return runtime.jsonrpc.error(-32602, "division by zero", {"field": "b"})
    return params["a"] / params["b"]

def on_progress(params):
    # Notifications receive params but produce no response.
    pass

See the scriptling.runtime.jsonrpc reference for the full API.

Decorator Syntax

Instead of registering methods and notifications separately in a setup script, attach them directly to handler functions with decorators:

# handlers.py
import scriptling.runtime.jsonrpc as jsonrpc

@jsonrpc.method("echo")
def echo(params):
    return params

@jsonrpc.method("divide")
def divide(params):
    if params["b"] == 0:
        return jsonrpc.error(-32602, "division by zero")
    return params["a"] / params["b"]

@jsonrpc.notification("progress")
def on_progress(params):
    pass

The setup script imports the handler library, which fires the decorators:

# setup.py
import handlers  # decorators fire, methods are registered

When the file is imported, the decorators register each handler with the correct "module.function" reference. The imperative API (runtime.jsonrpc.method("echo", "handlers.echo")) continues to work unchanged. Both forms can coexist in the same project.

Talking to the Stdio Server

# Single request
echo '{"jsonrpc":"2.0","method":"echo","params":{"hello":"world"},"id":1}' \
  | scriptling --json-rpc setup.py
# {"jsonrpc":"2.0","result":{"hello":"world"},"id":1}

# Batch (returned as a single JSON array)
echo '[{"jsonrpc":"2.0","method":"divide","params":{"a":1,"b":0},"id":1},
       {"jsonrpc":"2.0","method":"echo","params":42,"id":2}]' \
  | scriptling --json-rpc setup.py
# [{"jsonrpc":"2.0","error":{"code":-32602,"message":"division by zero","data":{"field":"b"}},"id":1},
#  {"jsonrpc":"2.0","result":42,"id":2}]

# Notification (no id): no response is written
echo '{"jsonrpc":"2.0","method":"progress","params":{"done":1}}' \
  | scriptling --json-rpc setup.py

Talking to the HTTP Server

# Single request
curl -X POST http://127.0.0.1:8000/json-rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"echo","params":{"hello":"world"},"id":1}'
# {"jsonrpc":"2.0","result":{"hello":"world"},"id":1}

# Batch. Notifications inside the batch are handled but omitted from the response.
curl -X POST http://127.0.0.1:8000/json-rpc \
  -H "Content-Type: application/json" \
  -d '[{"jsonrpc":"2.0","method":"divide","params":{"a":1,"b":0},"id":1},
       {"jsonrpc":"2.0","method":"progress","params":{"done":1}},
       {"jsonrpc":"2.0","method":"echo","params":42,"id":2}]'

# Notification-only requests return 204 No Content.
curl -i -X POST http://127.0.0.1:8000/json-rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"progress","params":{"done":1}}'

Concurrency

Each request is dispatched on its own goroutine with a fresh Scriptling evaluator, mirroring runtime.http, MCP, and WebSocket serving. Two slow handlers running back-to-back finish in roughly one handler’s time, not the sum.

Two limits keep one client from exhausting the server, over both stdio and HTTP:

  • Batch size. A batch of more than 1000 elements is refused before any of it runs, with a single Invalid Request error (-32600, “batch too large: N elements exceeds the limit of 1000”). Change it with --json-rpc-max-batch.
  • Concurrent handlers. At most 4 × the CPU count of handlers run at once, shared across every batch, request and stream. Further calls wait for a free slot rather than failing, so every call is still answered; a call whose request is cancelled while it waits fails with “server busy”. Change it with --json-rpc-concurrency.

Set either to a negative value (--json-rpc-concurrency=-1) to remove it.

Handlers cannot share in-memory state across requests. To coordinate between requests, use runtime.kv (with --kv-storage for persistence) or the runtime.sync primitives (WaitGroup, Queue, Atomic, Shared).

Error Handling

Code Meaning When
-32700 Parse error Malformed JSON on stdin or in the HTTP request body
-32600 Invalid request Not a valid JSON-RPC 2.0 request
-32601 Method not found No handler registered for method
-32602 Invalid params Params failed to decode, or via runtime.jsonrpc.error()
-32603 Internal error Request cancelled (e.g. signal)
-32000 Server error Handler raised an exception

Return runtime.jsonrpc.error(code, message, data=None) from a handler to emit a response with a custom error code and optional structured data.

See Also