Space Methods

Space methods let a running space publish JSON-RPC methods through Knot. Knot handles discovery, access control, MCP tool projection, and routing calls back to the space’s long-running stdio method server.


Registering Methods

Inside a space, create a TOML file:

[server]
type = "stdio"
command = "./bin/notes-rpc"
timeout = 30
mode = "concurrent"

[[methods]]
name = "{{space}}.search"
local_name = "search"
description = "Search indexed notes in this space"
keywords = ["notes", "search", "documents"]
scope = "private"
groups = []
mcp_tool = true

[methods.params_schema]
type = "object"
required = ["query", "tag"]

[methods.params_schema.properties.query]
type = "string"

[methods.params_schema.properties.tag]
type = "string"

[methods.params_schema.properties.limit]
type = "integer"
default = 10

[methods.result_schema]
type = "object"

Register the file from inside the running space:

knot methods register methods.toml

knot methods register also accepts a Scriptling file (methods.py). This allows the same configuration style as used in startup scripts to be used rather than maintaining a TOML equivalent:

knot methods register methods.py

Registering at agent startup

To register methods automatically when the agent starts — without running knot methods register by hand — point the agent at the file with --methods-file (or the agent.methods_file config key / KNOT_METHODS_FILE environment variable):

knot agent start --methods-file methods.toml

The file may be a .toml registration or a .py Scriptling script, exactly as with knot methods register. The agent waits until it has connected to the server, then registers once; any failure is logged and never stops the agent from serving the space. This is the convenient way to bake a method registration into a space image or template so the space’s methods come up on their own every time it starts.

The method server is a long-running stdio JSON-RPC process. Knot writes JSON-RPC requests to the server’s stdin and reads responses from its stdout, correlating each response by JSON-RPC id. Logs should be written to stderr.


Event Subscriptions

A method can subscribe to events by declaring events in its definition. When a matching event fires, Knot calls the method via JSON-RPC with the event payload as params. Optionally, event_sinks references named formatters that transform the payload before delivery.

events

Subscribes the method to event patterns. The method receives the default event payload format.

TOML:

[[methods]]
name = "handle_events"
local_name = "handle_events"
description = "React to space lifecycle events"
events = ["space.created", "space.started"]

Scriptling:

server.method(
    name="handle_events",
    description="React to space lifecycle events",
    events=["space.created", "space.started"],
)

The method receives a single params dict:

{
  "event_id":   "...",
  "event_type": "space.created",
  "event_ts":   "...",
  "data": { ... }
}

event_sinks

Optional formatters. References named JSON-RPC sinks defined on the Events page. Each sink has its own event patterns and a body template that formats the event data before passing it to the method. event_sinks does not subscribe the method — events is always required for subscription.

TOML:

[[methods]]
name = "handle_deploy"
local_name = "handle_deploy"
description = "Handle deployment events"
events = ["custom.deploy.*"]
event_sinks = ["deploy-formatter"]

Scriptling:

server.method(
    name="handle_deploy",
    description="Handle deployment events",
    events=["custom.deploy.*"],
    event_sinks=["deploy-formatter"],
)

When an event matches events, the server checks each referenced sink’s patterns in declaration order. The first matching sink’s body template formats the payload. If no sink matches, the default format is used.

See JSON-RPC Sinks for sink configuration details, scope rules, and delivery semantics.


Startup Script Registration

Startup scripts (and knot methods register file.py) use the agent-only knot.methods library. Construct a Server, attach one or more method definitions, then call register():

from knot.methods import Server
from knot.methods import schema as s

server = Server("./bin/notes-rpc", timeout=30)

server.method(
    name="{{space}}.search",
    local_name="search",
    description="Search indexed notes in this space",
    keywords=["notes", "search", "documents"],
    scope="private",
    groups=[],
    mcp_tool=True,
    params=s.object(
        query=s.string(),
        tag=s.string(),
        limit=s.optional(s.integer(), default=10),
    ),
    result=s.object(),
)
server.register()

The TOML and Scriptling examples above describe the same method.

A space can only have one active method registration at a time. Calling register() replaces everything the space previously registered — every method from any prior Server is removed and only the methods from the most recent register() call remain. If you need to publish methods from more than one process, combine them into a single Server and a single register() call. (This matches the TOML path: a knot methods register overwrites the space’s previous registration too.)

Calling unregister() with no arguments removes all methods and stops the method server process. Calling unregister("search") removes just that method and re-publishes the reduced set (removing the last method also stops the server).

Server constructor

Server(command, *, type="stdio", timeout=30, args=None, mode="concurrent")

type defaults to "stdio" (the only currently supported transport) and is optional. mode is "concurrent" (default) or "serial". See Concurrency Mode below.

args defaults to None and is normalized to an empty list internally; pass an explicit list to add arguments to the server command.

Schema builder

knot.methods.schema (conventionally imported as s) builds JSON Schema fragments for a method’s params and result. The common kwargs (description, default, enum, extra) are accepted by every builder; unknown kwargs raise an error. extra={...} is an escape hatch for less-common JSON Schema keywords, and explicit kwargs win over keys in extra.

Constraint kwargs are snake_case in the builder and emitted as JSON Schema’s camelCase equivalents (min_lengthminLength, additional_propertiesadditionalProperties, and so on). If a JSON Schema keyword isn’t in the curated list below, use extra={"keyword": value}.

  • s.string(*, description, default, enum, format, min_length, max_length, pattern, extra)
  • s.integer(*, description, default, enum, minimum, maximum, extra)
  • s.number(*, description, default, enum, minimum, maximum, extra)
  • s.boolean(*, description, default, enum, extra)
  • s.null(*, description, default, enum, extra)
  • s.array(items, *, description, default, enum, min_items, max_items, extra)
  • s.object(**properties, *, description, default, enum, additional_properties, extra)
  • s.optional(schema, *, default=None)

Every property passed to s.object(...) is added to the schema’s required list unless it is wrapped in s.optional(...). additional_properties defaults to false.

If a builder does not cover a schema shape you need, pass a raw JSON Schema dict to params=/result= instead — both forms are accepted.


Concurrency Mode

The [server] table accepts a mode option that controls how many requests are in flight on the method server at once:

  • concurrent (default) — Knot may send many JSON-RPC requests to the method server before any response arrives. Responses are matched to callers by id and can arrive in any order. Use this for method servers that handle requests concurrently, or that are happy to read requests ahead of producing responses.
  • serial — Knot lets only one request be in flight on the method server at a time. The next request is sent only after the previous response is received. Use this for method servers that are not re-entrant.
[server]
type = "stdio"
command = "./bin/notes-rpc"
timeout = 30
mode = "concurrent"

In both modes the server’s timeout applies to each call.


Scriptling Method Servers

A Scriptling script can be the method server process itself, using the scriptling.runtime.jsonrpc library. The registration metadata (name, local_name, description, params_schema, scope, etc.) is declared in Knot exactly the same way as for any other method server — the only difference is that [server].command runs a Scriptling JSON-RPC server.

The Knot agent embeds the Scriptling runtime, so you do not need to install a separate scriptling binary in the space. Run the script through the agent instead: knot run-script ./setup.py --json-rpc.

knot run-script mirrors the Scriptling CLI’s run modes (container support excluded): --json-rpc (stdio JSON-RPC method server), --listen :PORT (HTTP server), and --mcp-tools DIR (MCP server), plus the sandbox flags (--allowed-path, --disable-lib, --bearer-token, --web-root, --kv-storage, --tls-*). Knot’s own libraries (knot.apiclient, knot.event, …) are available both to evaluated scripts and to served handlers (json-rpc/http/mcp). Run knot --version to see the bundled Scriptling version.

TOML form (methods.toml) — using the agent’s bundled runtime:

[server]
type = "stdio"
command = "knot"
args = ["run-script", "--json-rpc", "./setup.py"]
mode = "concurrent"

[[methods]]
name = "{{space}}.search"
local_name = "search"
description = "Search indexed notes in this space"
scope = "private"
mcp_tool = true

[methods.params_schema]
type = "object"
required = ["query", "tag"]

[methods.params_schema.properties.query]
type = "string"

[methods.params_schema.properties.tag]
type = "string"

[methods.params_schema.properties.limit]
type = "integer"
default = 10

Scriptling form (methods.py):

from knot.methods import Server
from knot.methods import schema as s

server = Server("knot", args=["run-script", "--json-rpc", "./setup.py"], mode="concurrent")

server.method(
    name="{{space}}.search",
    local_name="search",
    description="Search indexed notes in this space",
    scope="private",
    mcp_tool=True,
    params=s.object(
        query=s.string(),
        tag=s.string(),
        limit=s.optional(s.integer(), default=10),
    ),
    result=s.object(),
)
server.register()

Register with knot methods register methods.toml or knot methods register methods.py from inside the space.

To remove all methods and stop the method server:

knot methods unregister

setup.py routes each method’s local_name (the name Knot forwards over stdio) to a handler function referenced as "library.function":

import scriptling.runtime as runtime

# "search" matches the local_name of the registered method.
runtime.jsonrpc.method("search", "handlers.search")

And handlers.py defines the handler. Each handler takes a params dict (matching the method’s params_schema) and returns a JSON-serializable result (matching result_schema). Handlers are referenced as "library.function" strings rather than closures so the server can spin up a fresh, isolated evaluator per request:

def search(params):
    query = params["query"]
    tag = params.get("tag", "")
    limit = params.get("limit", 10)
    # ...your search logic...
    return {
        "results": [
            {"title": "Receipt for " + query, "tag": tag},
        ],
    }

Each request runs on a fresh, isolated evaluator, so the server is concurrent by default — leave mode = "concurrent" (the default). See the Scriptling JSON-RPC example for a complete working server with notifications and structured errors.


Visibility

scope defaults to private.

  • private methods are visible only to the owning user.
  • shared methods are visible to users with the shared-method permission.
  • groups restricts a shared method to users in at least one listed group. Entries may be group names (e.g. "Group 3b") or group IDs (UUIDs); both are accepted. Unknown names fail registration so a typo doesn’t silently exclude every caller.
  • An empty groups list means all authenticated users in the zone with permission to use shared methods.

The owner always sees and calls their own methods under the bare canonical name — never under user.<self>.<name>. Other users always see and call a shared method under user.<owner>.<canonical>, regardless of whether the canonical name contains a dot. So a method registered as notes.search by paul is shown as notes.search to paul and as user.paul.notes.search to everyone else.


Duplicate Methods and Load Balancing

Multiple spaces can register the same canonical method name, as long as the visible method definition is identical. This is useful when several spaces provide the same service and should share calls.

When a call has more than one visible target, Knot selects the target with the fewest in-flight calls for that method. If multiple targets are tied, the one that registered first wins. This means long-running calls naturally push later calls toward less busy spaces.

Method registrations are held by the Knot server process that has the live agent session for the space. In a multi-server cluster, load balancing happens among the matching method registrations visible to the Knot server handling the request; method calls are not redistributed through a separate cluster-wide method registry.


Calling Methods

List visible methods:

knot method list

Show full details (params/result schemas, owner, scope, provider count) for a specific method:

knot method list notes.search

Call a method with JSON params:

knot method call notes.search '{"query":"receipt"}'

Or pipe params:

echo '{"query":"receipt"}' | knot method call notes.search | jq

Call the same method multiple times in one batch (each element in the params array becomes a separate call):

knot method call notes.search --batch '[{"query":"a"},{"query":"b"}]'

The HTTP API is:

GET /api/methods
POST /api/methods/call

POST /api/methods/call accepts a JSON-RPC 2.0 request:

{
  "jsonrpc": "2.0",
  "method": "notes.search",
  "params": { "query": "receipt" },
  "id": 1
}

Returns a JSON-RPC response:

{
  "jsonrpc": "2.0",
  "result": { "results": [ ... ] },
  "id": 1
}

Batch requests

Send a JSON array to call multiple methods in one request. Each item is routed independently using the same load-balancing rule as a single request — items can target different spaces and Knot naturally splits the batch by destination agent:

[
  {"jsonrpc":"2.0","method":"containers.search","params":{"query":"a"},"id":1},
  {"jsonrpc":"2.0","method":"user.paul.notes.search","params":{"query":"b"},"id":2}
]

Returns a JSON array of responses (order is not guaranteed by JSON-RPC 2.0 but Knot preserves request order):

[
  {"jsonrpc":"2.0","result":{...},"id":1},
  {"jsonrpc":"2.0","result":{...},"id":2}
]

Notifications

A notification is a JSON-RPC request without an id field. Knot forwards it to the method server but does not return a JSON-RPC response. For a single notification, the HTTP response is 204 No Content.

{
  "jsonrpc": "2.0",
  "method": "containers.healthcheck",
  "params": {}
}

Notifications can also appear inside a batch — they’re forwarded but produce no entry in the response array. If all items in a batch are notifications, the HTTP response is 204 No Content.


MCP Tools

If mcp_tool = true, Knot registers the method as a discoverable MCP tool for users who can see it. Dots in method names are rewritten to underscores for the MCP tool name:

notes.search -> notes_search

mcp_tool defaults to false.


Scoped API Tokens

By default, an API token inherits the full authenticated surface of the user it belongs to — every endpoint the user can reach, the token can reach. For machine-to-machine access (e.g. handing a token to an external consumer that only needs to call methods or use MCP), tokens can be scoped to limit which endpoint groups they can reach.

Scopes are set at token-creation time in the web UI (API Tokens → New Token) or via the API (POST /api/tokens with a scopes array). An empty or absent scopes field means unrestricted (full access). The available scopes are:

  • methods — discover and call space methods (GET /api/methods, POST /api/methods/call)
  • mcp — use the MCP server (POST /mcp)

A scoped token can combine multiple scopes. For example, a token with scopes: ["methods", "mcp"] can reach both the methods REST endpoints and the MCP endpoint. A token with scopes: ["methods"] can reach methods REST only — it will get 403 Forbidden on every other endpoint, including /mcp and /api/spaces.

Scope enforcement is global: the check runs inside the server’s authentication middleware and applies to every API route. Tokens without scopes (including all pre-existing tokens) are unaffected. Session cookies and agent tokens bypass the check entirely.

Typical setup

  1. Create a user (e.g. rpc-bot) with PermissionUseMethods and/or PermissionUseMCPServer, plus the group memberships needed for the target methods.
  2. Create an API token on that user. In the web UI, leave “Full Access” unchecked and tick the relevant scope(s).
  3. Hand the token to the consumer.

Even if the user account is later granted broader permissions, the scoped token still can’t exercise them — only the endpoint groups named in its scopes are reachable.


Schemas

params_schema (TOML) / params (Scriptling) and result_schema / result use JSON Schema. Knot validates that schemas are well-formed at registration time and passes them through to discovery and MCP. Runtime parameter and result validation is left to the method server.

In TOML, write the schema inline as shown in the registration example above. In startup scripts and methods.py files, prefer the knot.methods.schema builder. If the builder does not cover a case you need, fall back to a raw dict.

The supported schema types are object, array, string, integer, number, boolean, and null.