The Mills · Data & statistics

sparky

A local MCP server handing a model the reckoner dictionary, and the grounding to use it: mills/sparky

Summary

The same calculator halifax puts at a terminal, handed instead to a language model (an AI assistant) as callable tools. A student asks a question in English. The model answers it by computing, and it shows its working. Every word in the language carries its own stack effect and description, so the model never has to guess what a word does.

Named after Karen Spärck Jones (1935–2007). She gave information retrieval inverse document frequency (a way of scoring how telling a search word is) and spent a career arguing that computing was too important to be left to men. The name is apt twice over. Her subject was retrieving the right material for a question, and half this server's job is retrieving the right grounding for a word.

bin/mill run mills/sparky/sparky.shoddy     # the same dictionary, at a terminal
dotnet run --project hosts/mcp/sparky       # the server, on stdin and stdout

It folds thirty-eight of the reckoner's forty-one seeds. The three it leaves out are left out whole. Their words are not in the dictionary at all, rather than present and answering nonsense; see what is deliberately absent.

Registering it with an agent

Sparky is a local stdio server speaking MCP, the Model Context Protocol, the standard way an assistant launches a tool and talks to it. A client launches Sparky as a child process: a program started and owned by another program. The two speak JSON-RPC (messages written as structured text), one message per line, on stdin and stdout, the input and output streams every program has. Nothing listens on a network socket. So every client needs the same two things: the command to run, and its arguments:

sparky --root PATH

--root is where the session's files live: sparkyrc, anything save writes, any chart PLOTSAVE writes. Omit it and the server uses LocalApplicationData/Shoddy/Sparky, creating it if it is not there. Name a root that does not exist and the server refuses to start, saying which path it could not find. A root you named is one you meant, so a typo is reported rather than materialised as a new folder.

Get the executable from the releases page. Every release carries one sparky archive per OS (.zip for Windows, .tar.gz for Linux and macOS), each holding a single self-contained executable: a program that needs nothing else installed. Download, unpack, done. No repository, no .NET install, nothing else on the machine. The binaries are unsigned (they carry no publisher's certificate), so the first launch meets Windows SmartScreen (“More info”, then “Run anyway”) or macOS Gatekeeper (xattr -d com.apple.quarantine sparky). That happens once per download.

Or build it from the repo, which is the path for anyone changing it:

./build.ps1                                  # repo root: publishes bin/mill  (./build.sh elsewhere)
cd hosts/mcp
./build.ps1 release                          # sparky lands in artifacts/bin/sparky/release/
./build.ps1 publish                          # or the per-OS release archives, in artifacts/publish/

If you build from the repo, register a copy of the executable from somewhere outside the tree, not its artifacts/ path. Here is why. A client keeps the server running for its whole session, and a running server holds its files open. artifacts/ is exactly what the root build's clean deletes. So a registration pointing into the build tree makes every full rebuild collide with every open session.

The configuration, per client

Client configuration formats move faster than this page does. The contract above does not. If a form below has drifted, check that client's own documentation and give it the same command and arguments. All four forms are the same two facts under different keys.

Claude Desktop: claude_desktop_config.json (%APPDATA%\Claude\ on Windows, ~/Library/Application Support/Claude/ on macOS):

{
  "mcpServers": {
    "sparky": {
      "command": "C:\\shoddy\\artifacts\\bin\\sparky\\release\\sparky.exe",
      "args": ["--root", "C:\\Users\\you\\Documents\\sparky"]
    }
  }
}

Claude Code: one command, no file to edit:

claude mcp add --scope user sparky -- /path/to/sparky

--scope user is deliberate. It registers sparky once, at the top level of the user's configuration, for every folder and every surface Claude Code has, the terminal and the VS Code panel alike. The default scope files the server under the one project directory the command was typed in. That is the wrong home for a tool that is not about any project.

If the model's file tool is Fettler, give sparky a --root that Fettler declares as a tree, and the two can hand each other a file: Fettler writes data.json there, and the session runs "data.json" JSONLOAD. A directory of its own, outside any repository, is the right shape; the repository itself is not, because tapes, saved words and PNGs would land where git status reports them and a session's WRITELINES could reach source. Declare it in .fettler.local.json with create and update, and name the same directory after --root in the registration above.

VS Code (Copilot): the user-level mcp.json: Command Palette → MCP: Open User Configuration. A workspace can carry the same entry in .vscode/mcp.json instead. But a server that serves every folder belongs in the user file:

{
  "servers": {
    "sparky": {
      "type": "stdio",
      "command": "/path/to/sparky"
    }
  }
}

Anything else: a client that speaks MCP over stdio needs no adapter. Launch the executable, write one JSON-RPC message per line to its stdin, and read one per line from its stdout. Diagnostics go to stderr (the separate stream for error messages) and never to stdout, so nothing the server logs can corrupt the stream.

Checking it works without a client

Two lines on stdin are enough to see the whole handshake, the opening exchange in which the two programs introduce themselves:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"eval","arguments":{"lines":["{ 12500 13100 11900 } MEAN"]}}}' \
  | sparky --root /tmp/sparky

The second answer contains [stack] x: 12500. If it does, the dictionary is live. The client's only remaining job is to launch the same command.

The grounding prompt

The server serves this grounding itself, as the two resources sparky://grounding/reckoner and sparky://grounding/surface, so a client that reads resources needs nothing from this section. What follows is those two, concatenated and reproduced verbatim, for the case that has no server to ask: a system prompt, a project instruction file, or a model you are briefing before it ever connects. A test asserts this block and the served text are the same, so the copy below cannot drift from the one a client is given.

Two parts of it justify their length. The first is the instruction not to invent a word. Every word carries its own stack effect and description, read from the running engine, so a model that asks is never wrong, and a model that guesses is fluently, confidently wrong. The second is Where the calculator cannot catch you. Everything else a model gets wrong is met with a refusal that names the fix: wrong arity (the number of arguments a word takes), wrong type, wrong order, all caught. So the briefing can afford to be short about them. Those four are the ones that return a plausible number instead.

# The reckoner: how a line works

Lines are RPN (reverse Polish notation). Numbers and strings go on the
stack, and words work on what is there. `3 4 +` leaves 7. The stack is
shown after every line, with the top value labelled `x`, then `y`, `z`
and `t` beneath it.

- **A list is `{ 1 2 3 }`.** A program (a quotation) is `[ DUP * ]`.
  `{ 1 2 3 } [ 2 * ] MAP` answers `{ 2 4 6 }`.
- **A string is double-quoted.** It shows with its quotes on the stack
  and without them under `PRINT`.
- **You define a word with `: NAME ... ;`**, for example
  `: VAT DUP 0.2 * + ;`. It may span several lines. The definition is
  not run until it closes.
- **The line is the transaction.** A line either takes effect whole or
  is refused whole, and a refusal leaves the stack exactly as it was.
  Refusals start with `?:`.
- **Nothing you can type ends the session.** If something is wrong, you
  get a refusal and another chance. Trying a line costs nothing, so try
  it rather than asking.
- **`UNDO` restores a stack, not the world.** A file already written is
  still written. A line already drawn is still drawn. A `PRINT` already
  printed cannot be unprinted.

## Two rules that differ from other stack languages

- **A user word takes exactly one cell.** Not "as many as it pops": one.
  If a word needs a second argument, store it in a register first with
  `STO` and read it inside with `RCL`. A two-argument definition is the
  most common mistake here. The definition is accepted, and every call
  to it is refused. The refusal names the word inside it, such as
  `+ needs 2, the stack holds 1`, not the word you defined.
- **A word cannot call itself.** A definition is checked against the
  dictionary as it stands, so its body may name only words that already
  exist, never the name being defined. Loop with `TIMES`, `MAP`,
  `FILTER` or `FOLD` instead.

## Reading a stack effect

`HELP` answers one. `STO  ( x name -- )` means the word takes two cells
and leaves none. The order is the order you push them: the value first,
then the name. `RCL  ( name -- x )` takes one and leaves one.
`PLOTHISTOGRAM  ( plot xs bins -- )` takes three, in that order.

Argument order is the mistake you will make most often. Reading the
effect line prevents it.

## Never invent a word

`WORDS` lists everything, grouped by the seed it came from. `HELP NAME`
gives a word's exact stack effect and description. `VIEW NAME` shows a
definition you made. Use the `help` and `words` tools rather than
guessing that a word exists. Every word carries its own effect and
description, so there is never a reason to invent one.

## A refused line tells you the fix

This is why trying costs nothing and why this briefing can be short. The
engine knows far more about its own words than any prompt can carry, and
it says so. Real refusals, verbatim:

    12500 13100 11900 MEAN       ?: MEAN needs a LIST, got NUMBER
    { 1 2 3 4 } 2 2 MAT          ?: MAT needs a NUMBER, got LIST
    640 480 PLOTOPEN             ?: PLOTOPEN needs 3, the stack holds 2
    { 1 2 2 3 } 4 PLOTHISTOGRAM  ?: PLOTHISTOGRAM needs 3, the stack holds 2
    "3" 4 +                      ?: + is not defined for STRING and NUMBER

Each one names what was wrong. Send the corrected line. The stack is
exactly as it was. Do not apologise to the user for a refused line, and
do not describe it as a failure. It is how you find the right line.

## Where the calculator cannot catch you

Everything above is caught. These four are not: the line is accepted, an
answer appears, and it is the wrong answer. Read this section closely.

**Trig is in radians unless you say otherwise.**

    90 SIN            x: 0.8939966636      radians, and almost certainly not the question
    DEG   90 SIN      x: 1                 degrees

Set `DEG` before any trigonometry a person asked for in degrees, and say
in your answer which mode you used. `RAD` switches back.

**Sample and population statistics are different words.**

    { 2 4 4 4 5 5 7 9 } STDDEV     x: 2.138089935    sample, divides by n-1
    { 2 4 4 4 5 5 7 9 } STDDEVP    x: 2              population, divides by n

The same goes for `VAR` and `VARP`. Both are right answers to different
questions, and nothing will tell you that you chose the wrong one.
Decide which the student meant, and say which you used.

**`FIX` changes what is shown, not what is held.**

    2 FIX   3.14159265      x: 3.14

The stack still holds every digit, and `STD` shows them again. Never
read a `FIX`ed display back as the value.

**Numbers are binary floating point (IEEE doubles), so money needs its
own words.**

    0.1 0.2 +               x: 0.3
    0.1 0.2 + 0.3 =         x: False

The display rounds, but the comparison does not. For currency, use
`MONEY` and its words. They are exact, and they refuse a bare number so
you cannot mix the two by accident.

## A worked line or two

    { 12500 13100 11900 } MEAN
    x: 12500

A word that needs two arguments, with one stored first:

    0.2 "rate" STO
    : WITHVAT DUP "rate" RCL * + ;
    250 WITHVAT
    x: 300

A matrix, built from its row and column counts first:

    2 2 { 1 2 3 4 } MAT LINDET
    x: -2

A chart. The third line sends back the picture:

    640 480 "p" PLOTOPEN
    "p" { 1 2 2 3 3 3 } 4 PLOTHISTOGRAM
    "p" PLOTBLIT

# What this server is, and what it is not

Sparky gives you the Halifax dictionary as callable tools: the whole of
the reckoner's standard library at an RPN prompt. It also gives you the
grounding to use it correctly. It covers statistics, matrices, linear
algebra, linear and integer programming, finance, symbolic algebra,
neural nets, regular expressions, sparse matrices, CSV/JSON/XML/HTML,
indexed files, number bases and bit work, charts and turtle graphics,
geography, calendar dates, the sun, moon and planets, and sorting and
grouping lists by a key.

**Compute the answer. Do not estimate it.** That is the purpose of this
server: you have a calculator that shows its working, and a student is
better served by `{ 12500 13100 11900 } MEAN` than by your arithmetic.

## What is in the dictionary

Over eight hundred words, grouped by the seed each came from. This is the
index. `words` gives the full list, and `help` gives any one word exactly.

    core          arithmetic, comparison, stack shuffling, registers, UNDO
                  and REDO, TRACE, the tape, angle and display modes, SHAPE,
                  and the combinators MAP FILTER FOLD TIMES IFT IFTE
    builtin       string slicing and character codes, number parsing,
                  arrays, whole-file read and write, the clock
    seed-math     logs to a base, hypotenuse, distance, clamp, lerp, remap
    seed-stats    mean, median, spread, quantiles, correlation, normal, t
    seed-seq      ranges, length, reverse, concat, first and rest, sorting
    seed-str      case, trim, split, join, fixed-decimal text
    seed-money    exact decimal money, splitting a sum without losing a penny
    seed-matrix   building matrices, identity, transpose, multiply, dot
    seed-dict     string-keyed dictionaries, and DPATH through nested ones
    seed-file     line-oriented files, and whether one exists
    seed-clock    timestamps, monotonic ticks, elapsed time, durations
    seed-random   seeded generation, ranges, shuffling, sampling
    seed-csv      reading and writing CSV, columns, headers, filtering
    seed-bool     number bases, bit operations, masks, fields, logic gates
    seed-shaker   reversible obfuscation of a list, with a tamper checksum
    seed-json     parse, render, pretty-print, load and save JSON
    seed-xml      the same for XML
    seed-html     the same for HTML
    seed-simplex  linear programs, and MPS files
    seed-lin      linear algebra: determinants, inverses, decompositions,
                  eigenvalues, solving
    seed-eng      angles, complex numbers, statistics, number theory,
                  polynomials, calculus, units and physical constants
    seed-fin      interest, annuities, loans, NPV and IRR, depreciation,
                  day counts, bonds
    seed-alg      symbolic algebra: simplify, expand, factor, solve, and
                  first-order differential equations
    seed-neural   small neural networks: build, train, predict, score, save
    seed-recio    fixed-layout binary record files
    seed-isam     indexed files: keyed lookup, ranges, insert and update
    seed-scribbler a pixel canvas to draw on
    seed-net      outbound HTTP fetches
    seed-turtle   turtle graphics
    seed-plotter  charts: histogram, scatter, box, bar, pie
    seed-https    HTTPS fetches, and reading a response apart
    seed-regex    regular expressions: test, find, groups, replace, split
    seed-sparse   sparse matrices, stored by column
    seed-mip      mixed-integer programs
    seed-terminal PRINT
    seed-geo      distance, bearing and area on the globe, sunrise and sunset
    seed-julian   calendar dates, Julian day numbers, weekdays, date arithmetic
    seed-ephemeris the sun, moon and planets: positions, phases, rising and setting
    seed-sinq     sorting and grouping by a key, distinct items, set operations,
                  batches
    shell         SAVE, LOAD, TAPESAVE, RESET and PUT, called as tools

Ask `subject` for any of those to get a teaching card: what the machine
is for, worked examples that run at this prompt, and its live word list.

## Resources are opened under a name, not held on the stack

A canvas, a plotter, a turtle, a record file and an indexed file are all
opened under a name you choose, and every later word takes that name:

    640 480 "p" PLOTOPEN
    "p" { 1 2 2 3 } 4 PLOTHISTOGRAM
    "p" PLOTBLIT

Nothing is pushed by the open. `BOUND` lists what is open, `CLOSE` shuts
one by name, and `CLOSEALL` shuts them all. This is why `UNDO` cannot
leave a resource open with nothing referring to it: what is open is not
on the stack in the first place.

## What is deliberately absent

| Not here | Why |
|---|---|
| sound | the server does not control the host's audio and cannot know if anything is listening |
| key handling | there is no keystroke source, so the words that classify keys classify nothing |
| terminal escape sequences | there is no screen, and a client receiving them in JSON is worse off than one told the word is absent |
| any way to read input | no seed registers `INPUT`, `INPUTLINE` or `INKEY` |
| any listening socket | `NETGET` and `NETREQUEST` are the whole of the network surface. There is no listen, accept or blocking-wait word |

These are absent from the dictionary, not merely withheld. Ask `HELP`
about one of their words and you are told it is not a word here.

## Five things that catch a text-only caller harder than a person

- **A string on the stack is rendered after every line.** The stack is
  shown whole with each answer, so a long string left at `y:` comes back
  with every later line until something drops it, abbreviated to one
  row but there every time. Bank what you are not using: `"doc" STO`,
  and `"doc" RCL` when you want it. `SHAPE` says what a value is, how
  long, which keys, without rendering it, and `DPATH` reaches a leaf in
  one line.
- **`UNDO` does not un-draw.** After `TURTLEFORWARD`, `UNDO` restores
  the stack and leaves the line on the canvas.
- **`UNDO` does not un-write, either.** A word that has already written
  a file has still written it.
- **`abandon` costs a turtle more than a chart.** A turtle's position,
  heading, pen and colour live in the session. A plotter has no state of
  its own, so a chart is one word to redraw and a turtle drawing is not.
- **`CLOSE` ends the drawing.** After it, the dictionary cannot reach
  the surface. This server keeps the last captured frame, though, so you
  can still be shown what was made.

## Pictures

Charts and turtle drawings go into a pixel buffer with no window. A line
ending in `PLOTBLIT`, `SCRIBBLIT` or `TURTLEBLIT` carries its picture
back in the same answer. A drawing you forgot to blit is not lost: ask
the `canvas` tool, which reads the buffer as it stands.

## Persistence

`SAVE`, `LOAD`, `TAPESAVE`, `RESET` and `PUT` are the server's words, not
the dictionary's. Call the tools of those names. Typing them in a line
gets you a refusal saying so.

`put` is the way in for text a line cannot carry. A string in a line
ends at the first closing quote and has no escapes, so a JSON document,
which needs double quotes, or anything long, cannot be typed. Call the
`put` tool with a name and the text: it lands in a register exactly as
`"name" STO` would have put it, and the line that uses it is
`"doc" RCL JSONPARSE`.

Text already in a file under the root needs no `put`. A line reads it:
`"data.json" READFILE "doc" STO`, or `"data.json" JSONLOAD` for JSON.
The root is named in the server's `initialize` instructions and in the
`save`, `load` and `tape` tool descriptions, so you can tell which
directory a file tool must write to for this session to read it.

Files live under one root the server owns. A plain name like
`"mine.sparky"` lands in it, and so does `"saved/mine.sparky"`. A path
that tries to leave it, such as `"../elsewhere"` or an absolute path
somewhere else, touches nothing outside the root. A read of such a path
is refused with `CANNOT READ ... (OUTSIDE THE FILE ROOT)`. A write, a
delete or `FILEEXISTS` answers `False` instead, with no refusal, so check
what a write answers. The `save`, `load` and `tape` tools keep to the
same root and refuse a path outside it. Use plain names.

Every word it exposes

Thirty-eight seeds and the shell's own words, as WORDS lists them, grouped by the seed each came from, in the order the dictionary was built. This is an inventory, for choosing what to reach for. It is not the reference. The running dictionary is, and help reads from it, so where this page and the server disagree the server is right.

-- core --
  + - * / ^ MOD WRAP ABS SGN FLOOR CEIL ROUND MIN MAX SQRT EXP LN LOG
  PI E = <> < > <= >= NOT AND OR TRUE FALSE DUP DROP SWAP OVER NIP
  TUCK ROT ROLL PICK DEPTH CLEAR GATHER UNPACK LASTX STO RCL CLOSE
  CLOSEALL BOUND UNDO REDO TRACE NOTRACE TAPE CLEARTAPE DEG RAD FIX
  SCI ENG STD SIN COS TAN ASIN ACOS ATAN ATAN2 IFT IFTE TIMES MAP
  FILTER FOLD HELP VIEW WORDS
-- builtin --
  LEN LEFT RIGHT MID INSTR INSTRFROM CHR ASC CODES FROMCODES CODEAT
  STR VAL VALOR ISNUMERIC NEGATE ATN ATN2 TANH ERF GAMMAP BETAI NTH
  SETNTH ISEMPTY PREPEND DIM READFILE WRITEFILE APPENDFILE DELETEFILE
  NETALLOWED ARGS CLOCK SLEEP CALL ERROR ASSERT
-- seed-math --
  LOGB LOG2 HYPOT DIST CLAMP LERP REMAP ROUNDTO
-- seed-stats --
  MEAN MEDIAN STDDEV STDDEVP VAR SUM QUANTILE CORREL NORMCDF NORMINV
  TCDF
-- seed-seq --
  RANGE LENGTH REVERSE CONCAT FIRST REST SORTL PAIR HEAD TAIL
-- seed-str --
  STRCAT UPPER LOWER TRIM SPLIT JOIN TOFIXED
-- seed-money --
  MONEY MADD MSUB MSPLIT MFMT
-- seed-matrix --
  MAT IDENT TRANSP MMUL MATADD MGET DOT
-- seed-dict --
  DHAS DGET DPUT DKEYS
-- seed-file --
  READLINES WRITELINES APPENDLINE FILEEXISTS
-- seed-clock --
  STAMP STAMPDATE STAMPTIME TICKS ELAPSED FORMATDURATION
-- seed-random --
  RANDOM RANDOMRANGE RANDOMINT SHUFFLE SAMPLE SEED
-- seed-csv --
  CSVLOAD CSVREAD CSVSAVE CSVWIDTH CSVHEADS CSVBODY CSVCOLUMN
  CSVCOLUMNNUMS CSVGET CSVWHERE CSVPAIRS
-- seed-bool --
  HEX BIN INBASE FROMBASE BITAND BITOR BITXOR BITNOT SHL SHR MASK
  GETFIELD BITAT POPCOUNT NAND NOR XOR XNOR IMPLIES IFF ROTL ROTR SHRA
  SIGNED UNSIGNED SIGNEXTEND NEGW ADDW SUBW MULW CARRIES OVERFLOWS
  CARD CARDWITH LOWBIT HIGHBIT GRAY FROMGRAY PARITY PARITYBIT HAMMING
  BCD FROMBCD MINTERMS IMPLICANTS TABLE
-- seed-shaker --
  SHAKE UNSHAKE UNSHAKEOR SHAKEOK SHAKEMOD
-- seed-json --
  JSONPARSE JSONTEXT JSONPRETTY JSONLOAD JSONSAVE
-- seed-xml --
  XMLPARSE XMLTEXT XMLPRETTY XMLLOAD XMLSAVE
-- seed-html --
  HTMLPARSE HTMLTEXT HTMLPRETTY HTMLLOAD HTMLSAVE
-- seed-simplex --
  LPPROBLEM LPSOLVE MPSPARSE MPSPARSEZERO MPSLOAD MPSLOADZERO MPSNAMES
-- seed-lin --
  LINTRACE LINNORM LINMAXABS LINISSQUARE LINISSYM LINREF LINRREF
  LINRANK LINDET LINADJ LININV LINCHOL LINLUOF LINQROF LINCHARPOLY
  LINEIGVALS LINEIGSYM LINSWAPROWS LINSCALEROW LINSCALECOL LINCOLS
  LINAUG LINSOLVE LINPOW LINVNORM LINVUNIT LINCROSS
-- seed-eng --
  ENGRAD ENGDEG ENGGRADOF ENGFROMGRAD ENGDMSOF ENGFROMDMS ENGSEC
  ENGCSC ENGCOT ENGASEC ENGACSC ENGACOT ENGANGLEOF ENGHYPOT ENGDISTOF
  ENGRECTX ENGRECTY ENGPOLAROF ENGSINH ENGCOSH ENGSECH ENGCSCH ENGCOTH
  ENGASINH ENGACOSH ENGATANH ENGASECH ENGACSCH ENGACOTH ENGLOGBASE
  ENGLOG2 ENGLOG1P ENGEXPM1 ENGNTHROOT ENGCADD ENGCSUB ENGCMUL ENGCDIV
  ENGCONJ ENGCABS ENGCARG ENGCFROMPOLAR ENGCTOPOLAR ENGCEXP ENGCLOG
  ENGCSQRT ENGCPOW ENGCNEAR ENGCSHOW ENGSUM ENGMEAN ENGSTDDEV
  ENGSTDDEVP ENGVAR ENGVARP ENGMEDIAN ENGQUANTILE ENGMINIMUM
  ENGMAXIMUM ENGRANGEOF ENGSKEWNESS ENGKURTOSIS ENGWEIGHTEDMEAN
  ENGGEOMEAN ENGCORREL ENGCOV ENGSPEARMAN ENGSTAT1 ENGLINEAR
  ENGLOGMODEL ENGEXPMODEL ENGPOWERMODEL ENGFITOF ENGNORMPDF ENGNORMCDF
  ENGNORMINV ENGTCDF ENGTINV ENGCHI2CDF ENGFCDF ENGBINOMPDF
  ENGBINOMCDF ENGPOISSONPDF ENGPOISSONCDF ENGGEOMPDF ENGGEOMCDF
  ENGHYPERPDF ENGEXPCDF ENGGCD ENGLCM ENGIQUOT ENGIREM ENGMODPOW
  ENGMODINV ENGISPRIME ENGPRIMES ENGFACTORIZE ENGDIVISORS ENGTOTIENT
  ENGFACT ENGCOMB ENGPERM ENGFIB ENGGAMMA ENGBETA ENGPOLYEVAL
  ENGPOLYTRIM ENGPOLYDEGREE ENGPOLYADD ENGPOLYNEG ENGPOLYSUB
  ENGPOLYSCALE ENGPOLYMUL ENGPOLYDIV ENGPOLYDERIV ENGPOLYINTEGRAL
  ENGPOLYAREA ENGQUADROOTS ENGCUBICREAL ENGLINSPACE ENGGEOMSPACE
  ENGCUMSUM ENGDIFF ENGMOVAVG ENGNORMALIZE ENGDOTOF ENGMAGOF ENGMETRE
  ENGKILOMETRE ENGCENTIMETRE ENGMILLIMETRE ENGINCH ENGFOOT ENGYARD
  ENGMILE ENGNAUTICALMILE ENGKILOGRAM ENGGRAM ENGTONNE ENGPOUND
  ENGOUNCE ENGSTONE ENGSECOND ENGMINUTE ENGHOUR ENGDAY ENGKELVIN
  ENGCELSIUS ENGFAHRENHEIT ENGPASCAL ENGKILOPASCAL ENGBAR ENGPSI
  ENGATM ENGJOULE ENGKILOJOULE ENGCALORIE ENGKWH ENGBTU ENGWATT
  ENGKILOWATT ENGHORSEPOWER ENGLITRE ENGMILLILITRE ENGCUBICMETRE
  ENGGALLONUK ENGGALLONUS ENGMPS ENGKPH ENGMPH ENGKNOT ENGTOBASE
  ENGFROMBASE ENGCONVERT ENGLIGHTSPEED ENGPLANCK ENGGRAVITATION
  ENGSTDGRAVITY ENGELECTRONCHARGE ENGELECTRONMASS ENGPROTONMASS
  ENGAVOGADRO ENGBOLTZMANN ENGGASCONSTANT ENGSTEFANBOLTZMANN
  ENGFARADAY ENGVACUUMPERMITTIVITY ENGVACUUMPERMEABILITY ENGE ENGTAU
  ENGROUNDTO ENGFRACOF ENGCLAMP ENGLERP ENGINVLERP ENGREMAP
  ENGSMOOTHSTEP ENGSIGN ENGNEAR ENGSIGFIG ENGSCI ENGCOMMAS ENGDERIV
  ENGDERIV2 ENGINTEGRATE ENGSUMOF ENGPRODOF ENGZEROOF ENGMINOF
  ENGMAXOF
-- seed-fin --
  FINPCT FINASPCT FINEFF FINNOM FINRULE72 FINREAL FINPCTCHG FINTOBPS
  FINFROMBPS FINPV FINFV FINPVANN FINFVANN FINPVANNDUE FINFVANNDUE
  FINPMT FINPMTDUE FINNPER FINSIMPLE FINCOMPOUND FINSUM FINMEAN
  FINSTDDEV FINSTDDEVP FINVAR FINVARP FINMEDIAN FINQUANTILE FINMINIMUM
  FINMAXIMUM FINRANGEOF FINSKEWNESS FINKURTOSIS FINWEIGHTEDMEAN
  FINGEOMEAN FINCORREL FINCOV FINSPEARMAN FINNORMP FINTDIST FINSTAT1
  FINLINEAR FINLOGMODEL FINEXPMODEL FINPOWERMODEL FINFITOF FINGOALPMT
  FINRETIRE FINNETWORTH FINBUDGETOF FINDCAOF FINBALANCE FINAMORTAT
  FINSCHEDULE FINEXTRAPAYOFF FINCARDPAYOFF FINCARDMINPAYOFF FINNPV
  FINNPVOF FINIRR FINNFV FINMIRR FINPAYBACK FINPAYBACKDISC FINCAGR
  FINROI FINRSTAT FINYIELD FINDIVGROWTH FINMARGINOF FINMARKUPOF
  FINSELLFROMMARGIN FINCOSTFROMMARGIN FINBREAKEVENOF FINPAYROLLOF
  FINSL FINSLBOOK FINSYD FINDB FINFACT FINCOMB FINPERM FINACTUAL
  FIN360 FIN365 FINANNUAL FINSEMI FINQUARTERLY FINDATE FINDATESHOW
  FINDATEOF FINISLEAP FINDAYSINMONTH FINSERIAL FINFROMSERIAL
  FINDATEADD FINDATEADDMONTHS FINDAYS360 FINDATEDIFF FINYEARFRAC
  FINCOUPONSBEFORE FINLASTCOUPON FINACCRUED FINBONDPRICE FINYTM
  FINMDUR
-- seed-alg --
  ALGSIMPLIFY ALGEXPAND ALGFACTOR ALGSOLVE ALGDESOLVE
-- seed-neural --
  NEURALNEW NEURALNEWSCALED NEURALPREDICT NEURALTRAIN
  NEURALREGRESSPLAN NEURALCLASSPLAN NEURALFIT NEURALPROBS
  NEURALCLASSOF NEURALSOFTMAX NEURALARGMAX NEURALONEHOT NEURALMSE
  NEURALACCURACY NEURALLOGLOSS NEURALCLASSACCURACY NEURALSCALERFIT
  NEURALSCALERAPPLY NEURALSAVE NEURALLOAD NEURALSAVEBIN NEURALLOADBIN
  NEURALMODEL NEURALMODELSAVE NEURALMODELLOAD NEURALMODELPREDICT
-- seed-recio --
  RECOPEN RECCOUNT RECFIELDS RECGET RECALL RECPUT RECAPPEND
-- seed-isam --
  ISAMOPEN ISAMCOUNT ISAMFIELDS ISAMHAS ISAMGETOR ISAMALL ISAMRANGE
  ISAMINSERT ISAMUPDATE ISAMDELETE
-- seed-scribbler --
  SCRIBOPEN SCRIBWIDTH SCRIBHEIGHT SCRIBTITLE SCRIBPIXEL SCRIBFILL
  SCRIBTEXT SCRIBGETPIXEL SCRIBBLIT SCRIBSAVE
-- seed-net --
  NETGET NETREQUEST
-- seed-turtle --
  TURTLEOPEN TURTLEFORWARD TURTLEBACK TURTLERIGHT TURTLELEFT
  TURTLESETHEADING TURTLEHOME TURTLEPENUP TURTLEPENDOWN TURTLEGOTO
  TURTLESETPEN TURTLEWHERE TURTLEBLIT TURTLESAVE
-- seed-plotter --
  PLOTOPEN PLOTTITLE PLOTBLIT PLOTSAVE PLOTHISTOGRAM PLOTSCATTER
  PLOTBOX PLOTBAR PLOTPIE
-- seed-https --
  HTTPSTATUS HTTPREASON HTTPBODY HTTPHEADERS HTTPHEADER HTTPSGET
  HTTPSREQUEST
-- seed-regex --
  RXTEST RXFIND RXFINDALL RXGROUPS RXREPLACE RXSPLIT RXQUOTE RXWHY
-- seed-sparse --
  SPARSE SPDENSE SPENTS SPNNZ SPGET SPCOLIX SPCOLVS SPCOLDOT SPMATVEC
  SPVECMAT SPTRANSP SPADDROW SPADDCOL
-- seed-mip --
  MIPPROBLEM MIPSOLVE MIPFIXED MPSMIP MPSMIPLOAD
-- seed-terminal --
  PRINT
-- seed-geo --
  GEODIST GEOBEARING GEODEST GEOMID GEOROUTELEN GEOCROSSTRACK
  GEOBOUNDS GEOAREA GEOSIMPLIFY GEODMS GEOPARSE GEOHASH GEOLOCATOR
  GEOCOMPASS GEOJULIAN GEOSUNRISE GEOSUNSET GEODAYLENGTH GEOHORIZON
-- seed-julian --
  JULDATE JULDATESHOW JULDATEOF JULISLEAP JULDAYSINMONTH JULSERIAL
  JULFROMSERIAL JULDAYNUMBER JULADDDAYS JULADDMONTHS JULDIFF
  JULWEEKDAY
-- seed-ephemeris --
  EPHSIDEREAL EPHSUN EPHMOON EPHMOONDIST EPHMOONPHASE EPHMOONAGE
  EPHMOONRISE EPHMOONSET EPHMERCURY EPHVENUS EPHMARS EPHJUPITER
  EPHSATURN EPHPLANET EPHALTAZ EPHSEPARATION
-- seed-sinq --
  SORTBY SORTBYDESC GROUPBY MINBY MAXBY FINDFIRST TAKEWHILE SKIPWHILE
  DISTINCT UNION INTERSECT EXCEPT CHUNK
-- shell --
  SAVE LOAD TAPESAVE RESET

The tools

ToolWhat it does
evalEvaluate lines in order; answers what each printed, said, and left on the stack. A drawing blitted during the line comes back as a picture in the same answer. What a turn prints is carried to 200 lines or 16,000 characters, and the answer says how many lines were left out.
defineA whole : NAME … ; at once.
putText into a register, whole, past the tokenizer: the way in for a JSON document or anything a line cannot carry. "doc" RCL reads it back. Text already in a file under the root needs no put: "data.json" READFILE reads it in a line.
stackThe stack as it stands.
helpOne word's exact stack effect and description, read from the running dictionary.
viewThe body of a word the session defined.
wordsEvery word, grouped by seed, or by what touches the world.
canvasThe current drawing as a PNG, blitted or not.
save / loadWrite the session's definitions to a file under the root, and read them back. The descriptions name the root, and a path outside it is refused.
tapeEvery line entered and what it answered, one row each, counted from 1. With from and to, only those rows and a line saying how many the tape holds; with a file, written out under the root.
machines / subjectWhat can be taught, and a teaching card for one of them: the page's prose with the live word list.
resetA fresh dictionary: empty stack, no tape, only the words it started with.
abandonA new engine, for a session stuck in a fetch that will never return.

Resources carry the grounding: sparky://grounding/shape, …/reckoner, …/surface, sparky://dictionary, and sparky://machines/{name} for a subject card. Two prompts come with it: teach for a lesson in one machine's subject, and check-my-working for a student who has an answer and wants it recomputed rather than agreed with.

What is deliberately absent

The seed fold is the only thing that decides which words exist. That is why Sparky is a mill of its own rather than a narrower grant over halifax's. A capability grant does not gate a dictionary: withholding scribbler from halifax would leave SCRIBOPEN registered, listed by WORDS, answered by HELP, and returning nonsense to whoever called it.

Left outWhy
seedbuzzerSound. The server does not control the host's audio lifetime, cannot know whether anything is listening, and has no use for it.
seedkeysPure, and pointless. It classifies keystrokes, and there is no keystroke source.
seedvt100Pure, and pointless. It builds escape sequences for a screen that does not exist; a client receiving them in JSON is worse off than one told the word is absent.

Ask HELP BUZZPLAY and you are told it is not a word in this dictionary. That is the accurate answer, and the one a model can act on. INPUT, INPUTLINE and INKEY needed no excluding: no seed registers them. Neither does any listening, accepting or blocking-wait word. seednet bridges the request half and nothing else, so the network surface is outbound by construction rather than by policy.

Files live under one root, and the root is a boundary rather than a base. A path climbing out with .., an absolute path elsewhere, or a symbolic link along the way (a file that is a pointer to somewhere else) touches nothing outside it. A read of such a path is refused. A write, a delete or FILEEXISTS answers False. See Capabilities and grants.

Running It

CommandWhat it does
./build.ps1 runThe dictionary at a terminal: the debugging affordance, not the product. End input (Ctrl-Z, or Ctrl-D) to leave. There is no QUIT: a server has no session to leave, and the dictionary a person types at must be the one a client is handed.
./build.ps1 testThe headless suite: no terminal, no display, no network, and no files at all.
./build.ps1 buildWeave the mill into bin/.
hosts/mcp/build.ps1The server: restore, build, test.

A sparkyrc in the root loads on the way in, exactly as halifaxrc does. Absent is silence. Present, it is loaded and the word count reported. One that will not load says so, and the session starts anyway. abandon reads it again, because that is a new session. reset does not, because RESET means a fresh dictionary and nothing else.

How It's Built

FileWhat's in it
mills/sparky/sparky-core.shoddyThe fold (thirty-eight seeds in halifax's own order) and the four words the host owns. Pure: nothing here reads, writes or prints.
mills/sparky/sparky.shoddyThe console shell: a prompt, and sparkyrc. Everything that touches the world.
mills/sparky/test.shoddyThe headless suite.
hosts/mcp/Shoddy.McpThe session, the headless canvas, the grounding index, the tools, and the protocol loop.
hosts/mcp/sparkyThe executable: arguments, the root, and the stdio streams.

It does not include halifax-core.shoddy, and that is the decision most likely to be undone by someone tidying. Halifax's layer is a terminal REPL, a read-evaluate-print loop, the machinery of a prompt: a banner, QUIT, the interception that lets SAVE mean something typed as a whole line. Sparky's caller is a model calling tools. Its door is the engine's own: RckEval, which reckoner calls the only door a shell needs. Borrowing the REPL would import a prompt model there is no prompt for.

The protocol is hand-rolled over System.Text.Json. A protocol SDK (a ready-made library for speaking MCP) exists only in preview versions. It would become this repository's most volatile dependency, and it would sit on its least stable layer. The cost of not taking it is a few hundred lines.

How It's Tested

The mill's own suite grades the fold in both directions. Every seed folded in is present and reachable. Every word of the three seeds left out is absent, which is the half that needs a test. A seed that quietly rejoined would leave a client holding words the server cannot supply, and nothing else in the tree would notice.

It also runs a fuzz list, a stock of deliberately broken input: every malformed line, every host word misused, and the algebra shapes nothing had ever fed it. Each is followed by a plain sum, so a refusal that lost the state shows up as the next line failing too.

The Machines It Uses

MachineWhy
cuttleCutStr is the cell the put tool banks.
dictDictPut on the registers is how put stores it, the same call STO makes.
reckonerThe whole calculator: the stack, the dictionary, the registers, UNDO, the tape, HELP and WORDS. RckEval is the door every tool goes through.
seqFilter and Length count the words a session has defined, for the load report.
strSplit and Trim turn sparkyrc into lines, taking the carriage return off a file written on another platform.

And, through the fold, every machine the thirty-eight seeds bridge, which is most of the library.