Developer tools

Connect an assistant to the engine

A local MCP server, so an AI assistant you connect it to can calculate a natal chart and read what differs between two calculation records. It speaks the Model Context Protocol over stdio: one process on your machine, started by whichever host you point at it. No listener, no port, no outbound request, no files read or written.

Unpublished candidate· zodiacs-mcp-server 0.1.0-rc.6. It is not on npm. Install it from the archive below, whose SHA-256 is published beside it.

The calculation is local. The conversation may not be.

The server runs on your machine and calculates there. Nothing reaches Zodiacs.org and nothing is saved. It writes one line to its diagnostic channel when it starts and one if the connection errors — your host may show those, or keep them in its own log — and they carry a version number and an error class, never a birth detail or a record.

That is not the same as a local AI experience, and the difference matters more than the first sentence. Whatever assistant you connect this to decides what reaches its model provider: your message, the arguments it builds for these tools, and the results it reads back. If the assistant runs in the cloud — and most do — assume the birth details in a request reach it, under that provider's terms rather than ours. A local calculation server does not make a cloud assistant local.

Two smaller points in the same family. A comparison reports the exactdifference between two charts, so anyone holding one of the two can reconstruct the other from it: safer to pass on than a full record, not anonymous. And a version, checksum or source URL inside a record you supply is a claim that record makes about itself — nothing here authenticates it.

Install

You need Node >=22 and a POSIX shell — macOS, Linux, or WSL or Git Bash on Windows. The block below is not PowerShell. Nothing here needs an account, a key or a network service at run time.

The download is pinned to the commit that holds it, so the bytes cannot change under the digest. The block compares that digest and stops if it does not match: it does not print it for you to check by eye. If verification fails, nothing is extracted and nothing is installed.

( set -eu
# POSIX shell — macOS, Linux or WSL. Not PowerShell. It runs in a subshell, so a
# failure stops the install without closing your terminal.
FILE='zodiacs-mcp-server-0.1.0-rc.6.tgz'
DEST='zodiacs-mcp-server-0.1.0-rc.6'
for path in "$FILE" "$DEST"; do
  if test -e "$path" || test -L "$path"; then
    echo "Stop: $path already exists here. Move or delete it, then run this again." >&2
    exit 1
  fi
done
# Anything this block creates, it removes if it does not finish. Without that a
# failed run leaves the archive and a half-filled directory in the way, and the
# next attempt stops on the guards above instead of retrying. The paths are
# absolute because the failure can happen after the cd below, where a relative
# name would resolve inside the very directory being removed.
BASE=$(pwd)
trap 'status=$?; if test "$status" -ne 0; then rm -rf "$BASE/$DEST" "$BASE/$FILE"; fi; exit $status' EXIT

curl --disable --fail --silent --show-error --location --proto '=https' --max-time 60 \
  'https://raw.githubusercontent.com/ZodiacsOfficial/site/460a8c5037fc3725c3ca576c0d6958f7be5a7887/public/examples/zodiacs-mcp-server-0.1.0-rc.6.tgz' -o "$FILE"
test -s "$FILE" || { echo "Stop: the download produced no file. Nothing was extracted and nothing was installed." >&2; exit 1; }

node --input-type=module <<'JS'
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
const file = 'zodiacs-mcp-server-0.1.0-rc.6.tgz';
const expected = '12382917242ab48cf70c4a872432d660cdc6ebdc1ff6146f31b2d89d8efa05f0';
const expectedBytes = 65743;
const bytes = readFileSync(file);
const stop = (why) => {
  console.error(`Stop: ${why}`);
  console.error('Nothing was extracted and nothing was installed.');
  process.exit(1);
};
if (bytes.length !== expectedBytes) stop(`incomplete or altered download, ${bytes.length} bytes, expected ${expectedBytes}.`);
const actual = createHash('sha256').update(bytes).digest('hex');
if (actual !== expected) stop(`this is not the published archive.\n  expected ${expected}\n  got      ${actual}`);
console.log(`Archive verified: ${actual}`);
JS

mkdir "$DEST"
tar -xzf "$FILE" --strip-components=1 -C "$DEST"
cd "$DEST"
npm ci --no-audit --no-fund
npm run verify
trap - EXIT
echo "Installed and verified. Connect it from there:"
echo "  cd $DEST"
)

npm run verify is the check that the install works, not a second integrity check: it launches the server as a real process, speaks MCP to it with the official client library, calls all three tools with synthetic charts, refuses two bad requests, and confirms the session still works afterwards. Seventeen checks, one line each, exit 0 when they all pass. It tests behaviour rather than identity, so a tampered archive can pass it — the digest comparison above is what establishes you have the published bytes.

npm ci installs exactly the tree recorded in thenpm-shrinkwrap.json inside the archive, so two people running these commands get the same dependencies. This package declares nopreinstall, install, postinstall orprepare script of its own.

The install brings fourteen packages. Three are the server's own, pinned exactly —@modelcontextprotocol/server 2.0.0,@modelcontextprotocol/core 2.0.0 andzod 4.6.5 — and the other eleven are development dependencies of the MCP client, which onlynpm run verify uses. npm ci --omit=dev installs the three and the server runs; only the verifier needs the rest. The engine (@zodiacs/engine 0.1.1-rc.6) and the comparison are already inside server.mjs, because neither is published and bundling them keeps one copy of the comparison rules rather than two. Node >=22.

Connect it to a host

Claude Code is the host this was tested against, from the extracted directory:

claude mcp add zodiacs -- node "$PWD/server.mjs"
claude mcp list

Expect zodiacs: node … - ✓ Connected. Add --scope projectto write it into a .mcp.json your repository shares, or--scope user for every project on the machine.

For Claude Desktop, VS Code or Cursor, add the same entry to the config file their own documentation names — claude_desktop_config.json undermcpServers, .vscode/mcp.json under servers,.cursor/mcp.json under mcpServers. Those locations come from their docs rather than from a run here, so they are not tested claims; Claude Desktop is macOS and Windows only and could not be exercised on the machine this was built on.

{
  "command": "node",
  "args": ["/absolute/path/to/package/server.mjs"]
}

Use an absolute path. The server needs no environment variables and is given none: there is no key, credential or secret anywhere in the package.

The three tools

get_capabilities
No arguments. The engine and adapter versions, the record schemas, every limit a request must respect, and the list of what this adapter does not do. Worth calling before the other two rather than guessing at supported options.
calculate_natal_chart
One chart from an instant and, optionally, coordinates. Returns the bodies, angles, cusps and aspects, plus whether the time was known and which house system was requested against which one was actually used. The full calculation record is a separate, explicit request.
compare_calculation_records
Two records, passed as content. Names every field that differs, then what accounts for it — reproduced by a local recalculation, reported by the records, a hypothesis that fits, or unresolved. By default it leaves out the values of rows carrying birth details or computed positions, keeping their labels and their numeric differences; ask for the values explicitly when you need to read them.

Ask your assistant, in your own words: calculate a natal chart for 1988-03-21T06:45:00Z at latitude 40.7128, longitude -74.0060, with Placidus houses. Those are round public coordinates for New York on an ordinary date — a synthetic chart, nobody's birth details. You should get twelve bodies, four angles and twelve cusps, with the Sun at 0.873337° and the ascendant at 274.046910°, and houses reading{ requested: "placidus", actual: "placidus", absenceReason: null }. Then drop the zone — ask for 1988-03-21T06:45:00 with noZ — and it refuses rather than guessing: utc must be an ISO-8601 instant with an explicit zone. That refusal is the limit you would meet first for real, because this adapter resolves no time zones.

compare_calculation_records reads the record format this site produces — zodiacs.natal-envelope.draft-v1, what the birth chart here offers for download, what the developer starter writes, and whatcalculate_natal_chart carries in the record field of its reply when you pass output: "record". That field holds the whole record, as text; the two keys beside it name the engine that produced it and the vocabulary it speaks. Pass the field, not the reply around it — the reply is a different shape and the comparison refuses it. The vocabulary is a Zodiacs-owned draft rather than an industry interchange format, so another program's export and a screenshot are both out of scope.

Every argument is bounded by a schema the host can read: an instant needs an explicit zone and falls within 1800 to 2199 — an accepted input range, not a range every date in which has been independently checked for accuracy; the engine's own receipts record broadDateRange as not certified. Coordinates come as a pair or not at all, the house system is one of two the engine computes, and a record is at most 64 KB of content. An argument the schema does not know is refused rather than ignored. A model-supplied value becomes a number, a date or one of a fixed set of option strings — never a command, a path, a module name, a URL or a package version, because the adapter imports nothing that could carry one out.

The comparison is the same code as the browser tool on this site, which is the reason the adapter lives in the site's own repository: one set of rules, not a copy that drifts.

A cause is called reproduced only when three things hold: both records name a version this installation actually has, each record's own recorded values — cusps, angles and body positions — are reproduced from its own declared inputs, and changing only the house system turns each chart into the other, checked in both directions so the answer cannot depend on which record you passed first.

That is a statement about the engine on your machine and about those values. It does not establish where either record came from, and nothing here can: a version, checksum or source URL inside a record is a claim the record makes about itself. If the two records claim different builds of the same version, the answer says so in its stated limits and the verdict still rests on the recalculation rather than on the claim. When the arithmetic works and only the identity behind it cannot be established, the cause stays a hypothesis and the answer says what the installed engine does, which is a different claim from saying that setting explains the difference.

A comparison's answer leaves out the values of rows carrying birth details or computed positions and keeps their labels, kinds and numeric differences. You supplied both records to the call, so repeating their contents back tells you nothing you did not have, while adding a second copy to whatever the answer travels through. That shortens what travels onward; it hides nothing from the assistant you are talking to, which already received both records. It is not anonymisation, and the default answer is not an anonymous artifact: the exact differences are still there, and anyone holding one of the two records can reconstruct the other from them.

Known limits

  • One chart at a time. No transits, progressions, returns or eclipses, and nothing that scans a date range.
  • No interpretation. Positions and differences, no readings.
  • No timezone resolution. Supply an instant with an explicit offset; the adapter does not turn a place name and a wall clock into a moment.
  • No file access and no fetching. Records are passed as content.
  • No cancellation and no timeout. A calculation is synchronous, so a timer could not stop it mid-way; the work is bounded by refusing unbounded operations — one chart per call, no searches, no date ranges.
  • No authentication of anything — not a record, not an engine version, not the claim that two records came from independent software. Two records from one engine agreeing shows consistency, not independent accuracy.
  • Two house systems, placidus and whole, because those are the two the engine computes.
  • No body-to-house mapping. The summary returns the cusps and the body longitudes; which house a body falls in is left to the caller.
  • No outputSchema on the tools. Arguments are schema-bounded; result shapes are documented rather than declared.
  • A single request longer than 1 MB is dropped rather than answered. The stdio framing is one JSON message per line, so a line that cannot be buffered cannot be refused with a reply; it is discarded at the next newline and the session carries on.

Uninstall

Nothing is installed outside the directory you extracted and nothing is written anywhere else — no config of its own, no cache, no database, no state. Remove the entry from your host (claude mcp remove zodiacs, or delete it from the config file you added it to) and delete the directory.

How it was tested

Recorded in the repository underdocs/platform/evidence/mcp-adapter/, kept as separate records because they establish separate things: the official client library against the real server process; the Claude Code CLI launching it and reporting it connected; a model actually calling the tools through that host; and a fixed synthetic corpus of ten record pairs with the classification each should receive, written before the candidate ran against any of them. That last one is a regression corpus rather than an accuracy rate — both sides of every pair come from the same engine, so it says nothing about how often the tool is right about a disagreement between independently authored software.

Every chart in every test is synthetic: round coordinates for well-known cities on dates chosen for what they exercise.

Building something on this? Open anissue on the site repository — what you connected it to, what you asked it for, and what it got wrong. Use a synthetic chart: a public issue should not carry someone's name or real birth details. Our own tests are not adoption and are not counted as any.