Skip to content

examples/mcp: the examples/sila machine served to AI assistants; the kind facts move into python/onemachine - #27

Merged
neu-rah merged 4 commits into
mainfrom
mcp-example
Oct 8, 2026
Merged

neu-rah merged 4 commits into
mainfrom
mcp-example

Conversation

@neu-rah

@neu-rah neu-rah commented Oct 8, 2026

Copy link
Copy Markdown
Member

What

One machine, two consumers. examples/mcp serves the examples/sila machine to AI assistants over MCP (Claude Code and others): a _get, _set or _fire tool per role, generated from the machine's description, with the bounds in each tool's JSON Schema. No new firmware.

To make that a second consumer and not a second copy, what a kind says about a role moves into python/onemachine (role_facts), and each consumer's table keeps only its protocol's words. examples/sila/gateway.py goes from 328 to 267 lines and generates the same feature definitions.

Commits

  1. python/onemachine: role_facts — the kind table (KINDS), Facts, KindError, role_facts; test/role/facts_check.py (46 checks), run by test/role/build.sh.
  2. examples/sila: the gateway reads the kind facts from python/onemachine.
  3. examples/mcp — server.py, requirements.txt, README.md.
  4. docs — consumers.md and the README name the example; rule 1 says where the kind facts come from.

Where the claims are

  • Kind facts: KINDS python/onemachine/machine.py:231, Facts :246, role_facts :288; the arithmetic between raw and presented values: presented :262, to_raw :265 (nearest integer of the decimal as written, halves away from zero), label_of :272, raw_of :276, pack_text :280, unpack_text :285.
  • SiLA gateway: its table KINDS examples/sila/gateway.py:60 holds the SiLA words only; model() :86 calls role_facts (:92) and then applies its own unit table (UNITS :71) and the finite-decimal rule (decimal_str :75); the setter and the publisher use Facts at :207-209 and :227-229.
  • MCP server: its table KINDS examples/mcp/server.py:43 holds the parameter names; Role :60; model() :147 calls role_facts (:153); Facts is used at :68, :111-116, :131, :139-142.
  • Tests: test/role/facts_check.py (the eight kinds of the examples/sila machine, the arithmetic, 14 refusals by name from edited descriptions, 7 field-shape refusals from stub schemas); plugged in at test/role/build.sh:77.
  • The MCP SDK's low-level Server does not validate arguments against inputSchema; server.py validates every argument itself (Role.refuse, examples/mcp/server.py:97-131).

Identity (ran)

  • SiLA: the eight feature definitions the gateway generates for the examples/sila machine are byte-identical to the ones origin/main generates (diff -r, 8 files).
  • MCP: the tools/list result and the instructions (captured over JSON-RPC, 16 tools, 434 characters of instructions) are byte-identical to those of the server before it was moved.
  • SiLA on a Nano (ATmega328P, examples/sila firmware from main, image 97f1f9ea…5614e, built from this branch's examples/sila): the full end-to-end run with an independent SiLA 2 client (unitelabs-sila 0.10.0, sila_base 58290aa): 124 checks as before. The only differences from the same run on main: three refusal messages are reworded (below), and one negative check, which patched to_raw in gateway.py, patches nothing now that to_raw is in python/onemachine; the same mutation made in Facts.to_raw fails the same duty edge case (SetValue(0.05) reads 0.0, not 0.1) on the Nano, and the specified rounding passes.
  • MCP on a Nano and on the host build: a client written independently (the MCP Inspector, TypeScript) plus a raw JSON-RPC client and the link trace: 66 checks on the Nano, 67 on the host build, all held; both mutation checks fail as they must (the above-maximum check removed lets lamp_set 250 reach the board, which clamps it; a schema without maximum changes tools/list; an answer from the previous report fails the set-after-set check).
  • Claude Code on a Nano, fresh session, only this server's tools, the seven requests of the README: six correct, 4 frames on the link, and "set the lamp to 300" declined and asked about, nothing sent.
  • The shared layer is read by both: with Facts reporting a light's bound as max - 1 (a sed-patched copy), the Lamp definition changes to 199 and lamp_set's maximum changes to 199, and facts_check.py fails.
  • From a fresh clone of this branch with fresh virtual environments: both READMEs work as written, with the board and with the host build (claude mcp add then claude mcp list reports Connected).

Behaviour that changes

  • Three refusal messages of the gateway are reworded, because the check moved into the kind: ... which the X row does not use is now ... which the X kind does not use (a label on a discrete) and ... which the X kind does not print (a stray value, scale or unit line). They still name the role and the thing.
  • Refusals are now in a fixed order per role (kind and tuned, then the shared checks, then the consumer's unit table), so a machine with two problems may report the other one first.
  • A machine whose analog role has a scale that is zero or negative: the SiLA gateway used to accept it and the MCP server (before it was moved) refused it; both now accept it (the positive-scale rule is kept for scaled, where it orders the bounds).

Local test notes

test/role/build.sh locally: 34 ok, 1 failed. The failure, role_link_avr (AVR image size 6984 against the 6950 baseline), is the same on an untouched origin/main here (33 ok, 1 failed), so it is the local AVR toolchain and not this branch; CI's toolchain decides.

Not here

examples/mcp has no network transport, authentication, tuned roles, timeouts or reconnect (its README says so). The link helpers (Traced, open_link) are still a copy in each example.

🤖 Generated with Claude Code

neu-rah and others added 4 commits October 8, 2026 22:28
…ry consumer

A consumer's table is about its protocol's words. What a kind says about a
role does not depend on the protocol: the fields it reports and commands,
what bounds the command (a light's max), the values and labels, the scale and
unit, a text's length, and the arithmetic between a raw integer and a
presented value. role_facts(machine, role) reads them from the description
and the command and report schemas, and refuses by name a description that
does not hold what its kind needs. test/role/facts_check.py holds it to the
examples/sila machine (46 checks) and runs in test/role/build.sh.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Its table keeps the SiLA words (type, property, command, parameter); the
description's field names, the bounds, values, labels, scale, unit, the
text length and the presented/raw arithmetic come from role_facts. The
feature definitions it generates are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An MCP server (stdio) that reads the machine's description over the link and
generates a tool per role: <role>_get, <role>_set or <role>_fire, and status.
The schema of each _set is built from the kind and the description (a light's
level up to its max, a select's labels, a text's length, a scaled value's
bounds in its unit); a value outside it is refused with a tool error naming the
parameter and the bound, and nothing is sent. A set returns the value the
device holds. The kind facts come from python/onemachine; the table holds the
MCP words. Serves the examples/sila machine: no new firmware.

Promoted from R&D/OneMachine/mcpConsumer @ fce471e

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ough python/onemachine

consumers.md names examples/mcp as a second worked example and says, in rule 1
and in "Adding a consumer", that a kind's facts come from role_facts while the
consumer's table holds the protocol's words. The README lists the example.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@neu-rah
neu-rah merged commit b681c24 into main Oct 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant