Proposal: let a note expose typed operations that understand and safely transform that note's state. A caller asks for an operation such as chess.move instead of manually editing several representation blocks. The server validates the call, computes one state transition and commits all resulting edits atomically.
Related-system research and resulting refinements are recorded in the research companion.
{
"key": "chess",
"function": "chess.move",
"args": {
"colour": "white",
"from": "b1",
"to": "c3"
},
"if_rev": "r10",
"dry_run": false
}
The handler verifies whose turn it is, parses the canonical position, checks that the requested move is legal, produces the new position and move record, and updates the canonical state plus rendered board/log in one transaction. It returns the new revision, move notation, resulting position and a concise diff.
| Step | LLM environment and sheet behaviour |
|---|---|
| 1. Point | The user gives the environment a note key/link such as chess. The connector already exposes ordinary get plus the single generic notes.call tool. |
| 2. Read | The LLM retrieves the sheet. It sees human-readable rules/current board, canonical game state and the documented functions exposed by this sheet, for example move and resign. |
| 3. Orient | The LLM reads side-to-move, player/seat assignment, status, position revision/hash and move history. It treats board/log tables as projections and canonical state as authoritative. |
| 4. Choose | When it is the LLM's turn, the LLM itself chooses a move. No chess engine is implied; an engine could be a separate optional adviser. |
| 5. Call | The LLM invokes notes.call(key='chess', function='move', args={colour, from, to, promotion?, expected_position}, if_rev, request_id). It does not patch squares or append the log manually. |
| 6. Validate | The generic call core authorises the caller and loads the current revision. The trusted chess handler checks schema, game status, claimed colour/seat, turn, legal movement, check and promotion. |
| 7. Commit | The handler derives notation and the new canonical position, then atomically updates state, move history and all rendered projections. Illegal/stale calls commit nothing. |
| 8. Receipt | The call returns accepted/rejected, notation, game status, resulting position/hash, emitted event and new note revision. A rejection is structured enough for the LLM to correct its attempt. |
| 9. Display | The browser refreshes or subscribes to the new revision. Anyone reading the sheet sees the same position returned in the receipt. |
| 10. Continue | The LLM yields for the human move. On the next prompt/notification it gets the latest sheet rather than assuming its previous context is current, then repeats. |
| 11. Finish | Checkmate/stalemate is detected by move; resign(colour, expected_position) changes the terminal status atomically. Once terminal, further moves are rejected. |
{
"capabilities": [
{
"name": "move",
"description": "Play one legal move for the side whose turn it is",
"args": {
"colour": "white|black",
"from": "square",
"to": "square",
"promotion": "optional piece",
"expected_position": "position hash"
},
"changes_state": true,
"handler": "builtin:chess@1"
},
{
"name": "resign",
"description": "Resign the game as the named colour",
"args": {"colour": "white|black", "expected_position": "position hash"},
"changes_state": true,
"handler": "builtin:chess@1"
}
]
}
Colour is both a precondition and, only where identities are distinct, an authorisation check. In a casual session where human and LLM calls share one authenticated account, colour primarily catches accidental out-of-turn/wrong-side calls; it cannot prove which participant made the request. Strong seat enforcement requires distinct caller identities or an unforgeable per-seat capability token. The sheet should state which model it uses.
The sheet manifest is discoverable data, not executable code and not ambient authority. A malicious or mistaken sheet cannot invent a handler or broaden its effects: notes.call accepts only functions declared by that sheet and registered by the server, validates arguments independently, and confines returned changes to declared state/projections. The LLM may follow the game's documented protocol, but the server remains the enforcement boundary.
Treat a capable note as a small state machine: new_state = transition(old_state, function, arguments, context). The note holds state and declares which transitions apply. The executable implementation is selected by a trusted, versioned handler reference rather than evaluated directly from arbitrary note text.
| Part | Responsibility |
|---|---|
| Note | Canonical state, ordinary human-readable content, capability declarations and handler/version references. |
| Capability registry | Maps stable names such as chess.move to installed, reviewed handlers and argument/result schemas. |
| Invocation core | Authorises the caller, checks revision/preconditions, runs the handler, validates returned ops and commits atomically. |
| Handler | Parses domain state, enforces domain rules and returns structured edits/events; it does not write the database directly. |
| Renderer | Displays derived views and may generate forms/buttons from capability schemas. |
| Audit log | Records caller, capability, safe arguments, old/new revision, outcome and emitted events. |
Connector requirement: make only one planned MCP connector improvement—a single generic sheet/note function-call tool. Do not add one MCP tool per domain function, and do not require another connector change when a new sheet exposes different functions. Existing note reads provide discovery because the callable-function manifest and its documentation live in the sheet.
notes.call(key, function, args, if_rev, dry_run=false)
-> accepted/rejected, result, proposed/applied diff, events, new revision
# Discovery uses the existing notes.get(key):
# the sheet's capabilities manifest lists its callable functions and schemas.
HTTP may expose the same core as POST /{key}/call/{function} or POST /{key} {op:"call", ...}. MCP keeps one stable tool. The caller reads the sheet, selects one of the functions declared there, and calls it with schema-checked arguments. The browser can render the same declaration as controls without domain-specific connector code. A reserved function such as describe could return the manifest through the same call path, but is optional because ordinary note retrieval already exposes it.
Capabilities are not limited to LLM use. Both the wxPython Notes Browser and HTML renderer read the same sheet manifest and expose authorised entries under a Functions menu. A function without parameters can run directly after any required confirmation. A parameterised function opens a generated form based on its JSON Schema and nearby human documentation.
| Manifest schema | Generated control |
|---|---|
| boolean | Checkbox |
| enum | Choice/drop-down |
| string | Text field, with pattern/length validation |
| integer/number | Numeric control with minimum/maximum |
| square or other named format | Validated text initially; handler/plugin may later supply a richer picker |
| optional field | Clearly marked optional control |
| read-only function | Immediate result view; no state-change confirmation |
| state-changing function | Summary and confirmation, with current revision carried automatically |
{
"name": "move",
"handler": "builtin:chess@1",
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["colour", "from", "to"],
"properties": {
"colour": {
"type": "string",
"title": "Colour",
"oneOf": [
{"const": "white", "title": "White"},
{"const": "black", "title": "Black"}
]
},
"from": {
"type": "string",
"title": "From",
"pattern": "^[a-h][1-8]$",
"x-format": "chess-square"
},
"to": {
"type": "string",
"title": "To",
"pattern": "^[a-h][1-8]$",
"x-format": "chess-square"
},
"promotion": {
"type": "string",
"title": "Promote to",
"enum": ["queen", "rook", "bishop", "knight"]
}
}
},
"ui": {
"order": ["colour", "from", "to", "promotion"],
"submitLabel": "Play move"
}
}
Here oneOf produces a Black/White choice with stable submitted values and friendly labels; enum produces the promotion choices; pattern provides generic square validation; and x-format allows a richer chess-square picker when a client recognises it, while falling back to a text field elsewhere. The ui object is presentation-only. Colour remains a required assertion in every move call. The handler verifies three things independently: the caller/player is assigned that colour, the submitted colour matches that assignment, and it is currently that colour's turn. A browser that knows the player's assignment may preselect and lock the field to prevent accidental changes, but it still submits the value; an LLM must state it explicitly. Server validation always uses the registered handler's authoritative schema (identified by handler/version); the sheet may document it and narrow allowed options, but cannot broaden types/effects or bypass validation.
Submitting the form uses the same server-side call core as MCP—same authentication, authorisation, schema validation, if_rev/position preconditions, handler, atomic transition and audit receipt. On success the UI refreshes the sheet and shows the result; on stale state it refreshes before asking the user to retry; validation errors remain beside the relevant fields. The HTML form must work as a normal authenticated POST without JavaScript, with JavaScript only enhancing it. Include CSRF protection, an idempotency token and no sensitive parameters in URLs or ordinary access logs.
{
"capabilities": [
{
"name": "chess.move",
"handler": "builtin:chess@1",
"effects": {"read": ["state"], "write": ["state", "board", "game_log"]}
}
],
"state": {
"type": "chess.game@1",
"fen": "...",
"moves": ["d4", "d5", "b4", "e6", "Ba3", "Nf6"]
}
}
The FEN and move sequence are canonical; the board table and game log are projections. They may be materialised atomically for ordinary renderers, or generated at render time. A projection must never become an independently edited second source of truth.
Each exposed function must be documented in the sheet's capability manifest or by a nearby linked note. At minimum include: stable function name; purpose; argument names/types and required fields; result schema; whether it changes state; blocks/state it may read and write; permission needed; validation and failure conditions; idempotency/retry behaviour; handler version; and one small example. The declaration should contain enough machine-readable schema for MCP validation and enough plain-language text for a person viewing the sheet.
Ethereum smart contracts combine code and persistent state at an address; signed transactions call functions that apply deterministic state transitions, while read-only functions inspect without changing state. That is a useful conceptual model for capable notes. Ethereum's official documentation describes a smart contract as code plus data/state at an address, and transactions as state-changing instructions.
| Ethereum concept | Capable-note analogue |
|---|---|
| Contract address | Note key |
| Contract storage | Canonical note state |
| Contract code | Versioned capability handler |
| ABI | Function name plus argument/result JSON Schema |
| Transaction calldata | notes.invoke request |
| State transition/revert | Atomic structured-op batch or no change on failure |
| Transaction sender | Authenticated notes caller |
| Events/receipt | Audit events, result and new revision |
| View/pure function | Read-only capability or derived view |
| Code hash/version | Pinned handler version and migration record |
| State root/block revision | Document revision/content hash |
Do not import blockchain machinery that solves a different problem: no distributed consensus, mining/validators, gas market, public replication, cryptocurrency or irreversible global ledger is required. This notes service is centrally administered. The valuable lessons are explicit interfaces, deterministic transitions, atomic revert, event receipts, code/version identity and strict separation between caller input and persistent state.
The idea also resembles objects (state plus methods), actors (messages produce serial state transitions), database stored procedures/triggers, spreadsheets with formulas/macros, notebooks and document automation. The important design choice is not whether data and behaviour may coexist—it often does—but who may supply code, what it can access and how updates remain reviewable and recoverable.
| Level | Model | Recommendation |
|---|---|---|
| 1 | Declarative transition templates that return existing structured note ops. | Safest but limited; useful for simple append/set/workflow operations. |
| 2 | Installed server-side handlers/plugins selected by a note declaration. | Recommended first extensible implementation. Reviewed code, ordinary tests, bounded permissions. |
| 3 | Sandboxed code attached to a note, compiled to a constrained runtime such as WASM or a deliberately small language. | Possible later; requires quotas, deterministic APIs, signature/trust policy and migration tooling. |
| 4 | Arbitrary Python/Perl/shell eval or unrestricted webhooks. | Do not implement. It turns note editing into remote code execution and creates filesystem/network/secret risks. |
Every invocation should require authentication and capability-specific authorisation; validate arguments against a schema; use if_rev/ETag optimistic concurrency; optionally assert expected state (for chess: side to move and position hash); expose dry_run and diff; impose execution/time/output limits; declare read/write effects; deny filesystem, process, network, clock and randomness by default; and apply the returned structured ops through the existing atomic core.
Handlers must not receive raw database access. Their output is a proposed transition: result data, emitted events and a bounded list of structured operations. The core validates that the operations touch only declared blocks/state, then commits all or none. Retrying an invocation needs an idempotency key or semantics that make duplicate calls detectable.
Pin a handler version in the note. Upgrading behaviour is an explicit operation that can migrate state under dry-run/diff and record old/new handler identities. Do not let the registry silently change what an existing declaration means. Initially prohibit handler-to-handler calls; later composition can be added with an explicit call graph, depth limit and union of declared effects.
A chess handler is a strong pilot because legality is deterministic and the current manual update spans multiple blocks. Initial capabilities: chess.move(colour, from, to, promotion?), chess.position(), chess.legal_moves(square?), and optionally chess.undo(expected_last_move). The move capability validates colour/turn, legality, check state and promotion, then updates FEN, move history, board projection and log atomically.
Acceptance tests: legal move updates every projection; wrong colour, wrong turn, illegal move and stale revision change nothing; dry-run returns the exact proposed position; concurrent move loses on if_rev and can be recalculated; duplicate invocation does not create a second move; renderer refresh shows the same position returned by the handler.
1. Add canonical typed state, a documented capability manifest and handler references to JSONHTL without executing anything. 2. Make the connector's only planned extension one generic notes.call tool; use existing note retrieval for discovery and documentation. 3. Implement the shared call core around a small trusted handler registry. 4. Make handlers return existing structured batch ops and require if_rev, dry-run support and effect declarations. 5. Add manifest-driven Functions menus/forms to both the desktop browser and HTML renderer, submitting to that same call core. 6. Implement chess.move/resign as the pilot and derive its board/log from canonical state. 7. Only after trust, audit, versioning and migration are proven consider separately packaged plugins or sandboxed note-supplied code.