Agents
Four tools, three surfaces
MCP server, Pi extension and OMP extension all call the same executors in src/tool-operations.ts. They answer identically and a fix lands once.
| Tool | Does | Arguments |
|---|---|---|
chains_lookup | Resolve a chain and return its metadata | chain |
chains_validate_address | Check an address against one chain's format | chain, address |
chains_identify_address | Run an address through every validator | address |
chains_list | The registry, optionally one family | family optional |
chain is a key, name, symbol or alias, the same thing getChain takes. Every tool is annotated read only, idempotent and closed world, because none of them touches anything outside the process.
MCP
chains mcp
claude mcp add chains --scope user -- npx -y @agntn/chains mcp
Or in a client's config:
{
"mcpServers": {
"chains": { "command": "npx", "args": ["-y", "@agntn/chains", "mcp"] }
}
}
The server speaks MCP over stdio and checks every call against the tool's JSON Schema before an executor sees it: chain is 1 to 64 characters, address 1 to 256, family 1 to 32. A model can't hand the process a novel to decode. createMcpServer() is exported from @agntn/chains/mcp for hosts that bring their own transport.
What the text carries
An MCP client sees the text a tool returns and nothing else, so the text carries the whole answer. chains_lookup prints every metadata field on a hit:
Polygon PoS (polygon)
symbol: POL
decimals: 18
type: evm
chainId: 0x89
caip2: eip155:137
bip44: 60
explorer: https://polygonscan.com
rpc: https://polygon-bor-rpc.publicnode.com
Absent fields say so out loud, bip44: none (no registered SLIP-0044 coin type), rather than vanishing, because a missing coin type reads as "not shown" and invites the caller to supply one from memory. A derivation path on an invented coin type silently produces the wrong addresses.
When resolution fails the text names the registered keys, so the next call has somewhere to go. chains_list exists for the same reason: without it the only way to learn what the registry holds is to send a value you expect to fail.
What sets isError
A rejected address is an answer, not a tool error. chains_validate_address on a bad address returns Invalid Bitcoin (bitcoin) address: "…" with isError unset. Only an unresolvable chain or a chain with no validator sets isError, because then nothing was checked. chains_identify_address never sets it, a miss everywhere is still an answer.
The address in an answer comes back quoted. It arrives from whatever the caller was reading, and a newline inside one would otherwise write its own line, so a rejected address could read as a match on the chain about to be funded.
Pi and OMP
pi install npm:@agntn/chains
Both extensions are declared in package.json. They add details next to the text, the structured object the harness renders: the metadata for a lookup, { chain, address, valid, reason } for a check, the two key arrays for identify. MCP drops details and keeps the text.
The extensions prefer the built executors in dist/ and fall back to source only when it's missing. Without pnpm build the tools still register and the first call dies with a module resolution error, which is the kind of thing worth knowing before a demo.