The forge runs the first of .hanzo/workflows and .github/workflows that
exists. release.yml created .hanzo/workflows on 09-10, and from then on
publish.yml and cicd.yml never ran: @hanzo/mcp 2.4.10 sat on main while
npm served 2.4.9. All three lanes are in one directory now.
Co-authored-by: Hanzo Dev <[email protected]>
describe was filtered out of the hanzo tool's resources, and the default
surface has no other tool that reaches it, so a model there could list a
resource's actions but not learn what one takes. It is a resource again:
hanzo(resource="describe", args={subsystem, op}), and a resource's action
list points at it.
Arguments ride in args, one object. A string was spread into
{"0": "{", ...}, and a key beside args, input among them, was merged in as an
argument; both reached the fleet as some other call. Both are now refused as
INVALID_ARGS before anything is sent, and the data alias is gone.
Co-authored-by: Hanzo Dev <[email protected]>
70a0a04 put tgrep first in the grep tool's description and left the test
holding the old words, so the suite failed on main.
Co-authored-by: Hanzo Dev <[email protected]>
The catalog is regenerated from cloud (plugin/gen-mcp-catalog): the graph tool
now carries all thirteen operations, answer, communities, diff, erase, extract,
ingest and path among them, and the Rust crate's copy is synced from it.
The hanzo tool sent {resource, action, ...} to a subsystem handler that reads
{op, input}, so every call from the default surface reached the fleet with no
op. It now sends {op: action, input: args}, the shape the Rust runtime sends,
lists a resource's actions when none is named, and offers only subsystems.
Co-authored-by: Hanzo Dev <[email protected]>
--core-only set enableCore, which the unified surface ignores; it now offers the core group, and serve and list-tools read their flags through one function. list-tools printed four headers for tools that no longer exist and none of the 22; it now lists each tool under its group. install --claude-code wrote ~/.config/claude-code/mcp.json, which Claude Code does not read; it now runs claude mcp add --scope user. The smoke checks all three against dist/cli.js.
Co-authored-by: Hanzo Dev <[email protected]>
bad040d replaced hasRipgrep with searcher and left one caller, so tsc --noEmit failed before publish could build.
Co-authored-by: Hanzo Dev <[email protected]>
package.json pins typescript 7.0.2 and the lockfile still said ^7.0.2, so pnpm install --frozen-lockfile refused and publish stopped there.
Co-authored-by: Hanzo Dev <[email protected]>
The README said `npx -y @hanzo/mcp serve` runs an unrelated `serve`
package. With npm 11 it starts this server, and it is the form
`claude mcp add` writes; both forms answered initialize and tools/list
the same way from an empty npm cache, and claude mcp list reports the
added server connected.
The tool section now lists what serve offers, in the groups
src/tools/unified/index.ts defines, and gives the pipe that prints the
names. The "13 tools" and "260+" counts, a --legacy flag the CLI does
not have, the installer steps and the CI claims are gone: list-tools
prints category headers with no tools under them, install --claude-code
writes a file Claude Code does not read, and no workflow runs
smoke-mcp.mjs.
Co-authored-by: Hanzo Dev <[email protected]>
GitHub Actions is switched off for this repository on purpose — the API
reports enabled=false — so the lane I put under .github/ registered, showed as
active, and could never run. A pipeline that looks alive and does nothing is
worse than one that is plainly absent.
.hanzo/workflows is the native path: git.hanzo.ai runs it on our own fleet and
cd.hanzo.ai delivers. The runner labels follow the canonical <org>-build-<os>-<arch>
form, including hanzo-build-darwin-arm64, which is the target that was never
published and the reason for this lane.
The matrix key is `runner` rather than `on`. YAML 1.1 reads a bare `on` as the
boolean true, so as a key it comes back as True in anything that parses this
file — including the checks that read it.
Co-authored-by: Hanzo Dev <[email protected]>
gh release upload attaches to a release that exists. With the matrix creating
it, four jobs race and three lose — so one job opens it and the builds wait.
Idempotent on purpose: a re-run of a tag that already has a release attaches to
it rather than failing, which is what makes a retry after one target fails a
safe thing to do.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.sh offers `mcp` as a native download and names five published targets.
Nothing in this repository built any of them: hanzo.yml declares a TypeScript
gate, publish.yml ships the npm package, and the tarballs on the last release
covered Linux alone. On macOS the installer asked for a file nobody had built.
So this lane builds the Rust crate — the one whose Cargo.toml calls itself
hanzo-mcp — on the rust-v* tags it is already released under, and attaches each
tarball with the checksum the installer refuses to install without.
Both macOS targets build on the arm64 machine, because the Apple toolchain
cross-compiles to x86_64 without a second host.
fail-fast is off. One target failing leaves the others published, which is a
missing file somebody can chase; fail-fast leaves an empty release, which is an
install that breaks for everyone.
Co-authored-by: Hanzo Dev <[email protected]>
Trigram index prunes files a query cannot match: 23ms against ripgrep's 78ms
for an absent symbol, which is most of what a search asks. It loses where many
files match (128ms against 78ms), and with no index it scans everything and is
slower than plain grep — so the index is the condition, not the binary.
Co-authored-by: Hanzo Dev <[email protected]>
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.
Co-authored-by: Hanzo Dev <[email protected]>
The forge reads .github/workflows natively and github.com reads nothing else,
so one directory serves both planes instead of two conventions that disagree
about which one is live. The reusable in hanzoai/ci already lives there, so a
caller naming that path now resolves to a file that exists.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.
One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/ci keeps the reusable at .github/workflows/build.yml — the one path
both planes resolve, since github.com reads a reusable only from there and the
forge reads it too. The v1 tag moved with it, so a caller still naming
.hanzo/workflows/build.yml asks for a blob that is not in the tree:
InsertRun: read hanzoai/ci@v1:.hanzo/workflows/build.yml: object does not exist
The caller itself stays at .hanzo/workflows/cicd.yml on purpose: github.com has
no runner for these labels, so a caller placed there could never be scheduled.
Co-authored-by: Hanzo Dev <[email protected]>
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
The two lines had diverged on what this client offers: a hand-written tool
naming seven resources against four hosts, or the surface cloud generates
from its own typed operations. The generated one wins, and not by
preference — cloud's gen-mcp-catalog writes src/tools/catalog.json into
this repo, so the fleet already treats it as the destination.
hanzo-cloud.ts stays deleted and the Rust tool keeps reading the catalog.
The iam commits improved a `call` helper that dialled a host per service;
the catalog-driven path takes every address from the document and answers
through the error envelope, so what those commits were correcting is gone
rather than reverted. The hero card's font stack came across untouched.
384 JS tests, 51 Rust tests, typecheck clean.
Every suite had stopped executing. ts-jest peers on `typescript >=4.3 <7`
and this repo builds against 7, so all twenty files died inside ConfigSet
with "Cannot read properties of undefined (reading 'fileExists')" — twenty
failures, zero tests, and nothing to read them by because the push gate
deliberately runs typecheck and build rather than jest.
swc transforms instead: it strips types rather than asking the compiler
to, so it has no TypeScript version to disagree with, and `tsc --noEmit`
already answers the question ts-jest was answering twice.
Running as real ESM then exposed what the CommonJS fallback had hidden.
jest.mock does not hoist there, so a static import binds the real module
and the suite fails on `mockReturnValue is not a function` — an absent
mock reported as a broken one. The search, autogui and ui suites now
register through unstable_mockModule and import after it. One of them had
never mocked fs at all: it cast fs.promises to a mocked type and called
mockImplementation on the real module.
384 tests, twenty suites, all passing.
Regenerated by cloud's gen-mcp-catalog, which writes this file and its
siblings from the fleet's own typed operations. 115 subsystems, 1425
operations.
Three runtimes carry the file and each was refreshed on its own, which is how
they come to disagree. One run writes them all; a sibling repo that is not
checked out is skipped rather than created.
The registry fallback asked ui.hanzo.ai/api/registry, which 301s to a 404 — so a
dead address stood in for a second chance nobody had. The primary serves; the
fallback is gone rather than moved, because /v1/registry is not served either and
pointing a client at an address the estate does not answer is the same defect
with a better prefix.
The mode tests raced a process-wide singleton: one activates minimal while
another activates guido and reads it back, and cargo runs them in threads of one
process. Three runs gave ok, ok, FAILED. They share a lock now.
The runtimes are said to mirror one another, and this one listed ten services
against the fleet's 114. Two of the ten had been left behind, and the commerce
base was api.hanzo.ai/api/v1 — an /api/ prefix nothing serves.
Nine routers over four hosts become one dispatch to the single endpoint, and the
service list becomes a read of the file the TypeScript runtime already embeds.
The aliases are removed rather than repointed: three of the four named the old
surface, so they mapped a caller off the API. Spelling is still normalised.
The seven resources this offered had drifted from the fleet it names: paas is
platform, storage is s3, auth folded into iam, and iam alone carries 97
operations against the handful the hand-written tool reached. It also dialled
four hosts for one API.
The names come from cloud's typed operations now, so this client cannot come to
disagree with the API about what exists. The withholding rule is asked of cloud
rather than restated here — a client deriving its set from the raw catalog offers
what the fleet refuses.
The default surface is unchanged at 22 tools; the fleet sits behind hanzo, whose
resource enum is derived rather than listed.
runs-on was a bare scalar. Jobs written that way queue and are never
picked up — link and quest have waited since 10 August, painter since the
5th, none of them with a single success — while repos writing the same
label as a sequence drain within seconds of a push on the same fleet.
Same label, same runners, different shape. It fails silently: nothing errors,
the job simply never starts, so the lane looks configured and does nothing.
The social card asked for Inter first. That is not a face this estate
licenses, ships or ever drew, so a renderer with Inter installed painted
the wrong face and one without it silently fell through to system-ui.
Zen leads now; the same tail follows it.
Which list routes insist on an owner is the server's rule, and it moves. The
deployed binary requires one on users, sessions and applications and not on
roles, permissions, providers, invitations, tokens or audit-logs; the change
that resolves a listing's tenant centrally is written and unreleased.
So nothing here keeps a copy of that rule. `apps` no longer refuses a missing
owner before asking, and no description promises that leaving it off makes the
credential decide — today that answers 400, carrying the reason, which the
transport already surfaces.
Co-authored-by: Hanzo Dev <[email protected]>
Users, organizations, roles, applications, providers, permissions,
invitations, tokens, sessions and audit rows are addressed as
/v1/iam/{plural} for a list and /v1/iam/{plural}/{owner}/{name} for a row,
in both the Rust `hanzo` router and the TypeScript iam/auth tools. `id`
splits into its owner and name path segments and a malformed one is refused
rather than sent. Deleting a user is a DELETE on the row; updating one reads
the row and PUTs {"user": merged}.
`owner` is scope: it goes on the wire only when the caller names one, and the
server resolves the rest from the credential. /v1/iam/applications is the one
list that requires it, and asks.
A non-2xx answer is now an error carrying its status and reason — RFC 9457
`detail`, else the envelope `msg`, else `title` — so KMS, PaaS and IAM
refusals surface instead of reading as data. Tests cover the row address, the
id split, and a problem+json refusal against a local listener.
Identity questions belong to the iam service alone, so paas no longer
duplicates users/orgs/roles. Actions with no address behind them (enforce,
system_info, health, mfa status) are gone.
Co-authored-by: Hanzo Dev <[email protected]>
This repo is written on git.hanzo.ai and mirrored to github.com, and the two had drifted apart — 1 commits here the mirror lacked, 1 there this host lacked. Merging rather than overwriting so neither side's work is lost.
LLM.md is the source and stays the source; CLAUDE.md is a symlink to it, so there is
one file and it cannot drift from itself. What changes is that the link is committed
rather than left for each checkout to recreate.
Left untracked, the doc was conditional on a step outside the repo: a fresh clone had
no CLAUDE.md, nothing loaded LLM.md, and an agent worked here having never read what
this repo already explains. Most of the estate already tracks theirs; this is the rest
agreeing with them.
`fetch:search` scraped html.duckduckgo.com and pulled results out of a regex
over `class="result__title"`. Measured 2026-08-20: that page answers HTTP 202 —
DuckDuckGo's anti-bot challenge — and the markup carries no `result__title` at
all. So the regex matched nothing and the tool returned `{results: [], count: 0}`
for EVERY query: the identical answer it gives when the web genuinely has
nothing. Blind rendered as empty, with no way for a caller to tell them apart,
which is exactly why nobody noticed.
It now asks POST /v1/websearch — the meta-search we ship — reading the base URL
and credential from the same env the tracker tool reads, so the MCP has one way
to reach cloud rather than a second one here.
The failure shape is the point:
no credential -> NO_CREDENTIAL, never an empty result list
cloud refuses -> its own words verbatim ("sign in to search the web"),
because a 401 and a 500 are different problems and
flattening both into "search failed" makes an outage
look like a bad query
results -> carries WHICH ENGINES answered, so a zero-result search
with engines listed is a real empty and one with none is
a search that did not happen
HONEST GAP: the committed test cannot run. The repo declares typescript ^7.0.2
against ts-jest ^29.4.6, which does not support TS7, so `npx jest` dies in
_resolveTsConfig on "Cannot read properties of undefined (reading 'fileExists')"
— for the pre-existing suites too (edit.test.ts: 0 tests). `tsc --noEmit` is
clean repo-wide; only the transform is dead. The eight assertions were run
against the esbuild-bundled tool instead and all pass; they execute under jest
the moment the toolchain is fixed.
Co-authored-by: Hanzo Dev <[email protected]>
This repo is the only publisher of the package; the copy vendored in
hanzoai/extension is a build input and stays private. Records the flat KMS read
and that NPM_TOKEN sits at the org root, so the next reader does not rediscover
it against a 404.
/hanzo.yml declared a test block and nothing else, so this repo gated its
commits and never shipped them: 2.4.5, 2.4.6 and 2.4.7 all built green while
npm kept serving 2.4.4.
The lane follows @hanzo/logo, which publishes this way today. It asks the
registry whether the tree is ahead, typechecks and bundles (dist/ is
gitignored, so this is also what fills the tarball), reads NPM_TOKEN from KMS
with the machine identity the fleet already uses, and publishes. A version
already on the registry is skipped, so re-runs are safe.
The copy of this package vendored in hanzoai/extension stays private; it is a
build input, and this is the only place @hanzo/mcp publishes from.
The tool surface reaches a browser through zapd's native host, so the server
side of the localhost WebSocket transport has no counterpart. Removing it
takes `ws` out of the dependencies with it.
2.4.7.
`serve --transport http` answered "not yet implemented" and exited. It now
serves the streamable http transport through the SDK's StreamableHTTPServer
Transport: a fresh server and transport per request, stateless — no session
header, each POST answered on its own.
The request handlers are registered once, in registerHandlers, and both the
stdio and http paths call it. The tool surface a client sees is the same on
either transport.
Cloud's APIKeyPrefixes admits pk- and sk- only (auth_identity.go, 4b030b77).
Every tool here still told the user to set an hk- key, which cloud resolves to
no principal — following the message produced a key that could never
authenticate.
The message is now a single const beside the resolver that mints it, so the
seven tools that report it cannot drift apart again. Resolution order is
untouched: HANZO_API_KEY, then ~/.hanzo/config.json .apiKey.
Co-authored-by: Hanzo Dev <[email protected]>
The server replied "2024-11-05" to every initialize, whatever the client asked
for. `hanzo dev` asks for 2025-06-18 and refuses a server that answers anything
else, so it reported `MCP server hanzo failed to start` — in an image where the
binary was on PATH, `dev mcp list` showed it registered, and a hand-written
handshake got 29 tools back. Everything that looked at the server passed;
nothing that PAIRED it with an agent worked.
Negotiation is the whole fix: echo the requested revision when it is one we
serve, answer our newest when it is not and let the client decide. The three
listed revisions are the ones this server can actually serve — initialize,
tools/list and tools/call are unchanged across them, and what the later ones add
is optional and unadvertised. A client pinned to the original revision keeps
getting it, which is the point of negotiating rather than moving the constant.