Hazina MCP Integration Guide
What is the Hazina MCP?
McpServer + registerTool over StdioServerTransport, bridged into Windows by wsl.exe.
Built on @modelcontextprotocol/sdk 1.29 with Zod input schemas; stdout is reserved for the protocol so every log goes to stderr, and one stray console.log corrupts the stream. The vault is the Linux source of truth at ~/.codeamani/hazina, and its master key is DPAPI-wrapped — the Linux launcher shells back out to Windows powershell.exe over WSL interop to unwrap it, so key custody stays with the Windows account. Windows clients (Cursor's Grok bot, Claude Code desktop) reach it via wsl.exe -d Ubuntu -- /home/barnabas/.local/bin/hazina-mcp. Do not add an env block to that config: it applies to the Windows process and only crosses into Linux via WSLENV, so a HAZINA_HOME set there is silently ignored and you quietly bind against the wrong vault. For codeAmani this is the difference between convenience and exposure — Daraja consumer secrets and Stripe live keys are the crown jewels, and an M-Pesa credential in a summarized transcript is real money.
Five Hazina MCP primitives
Transport, trust boundary, and the bridge that joins two operating systems.
Hazina MCP Integration Guide
Focus — running codeAmani's first-party Hazina MCP server from WSL and exposing it to Windows-side agents (Cursor's Grok bot, Claude Code desktop) without ever moving the vault.
Overview
Most MCP servers in this stack are vendor-hosted and remote — you point a URL at them and attach a bearer token. Hazina is the opposite: a local stdio server you own, launched as a child process, speaking JSON-RPC over stdin/stdout. There is no network listener and no endpoint to leak.
It fronts the Hazina encrypted secrets vault. The defining design decision is that no MCP
tool returns a secret value. Agents get names, references, wiring graphs and readiness
reports; the only path a real value takes is inject, which writes a gitignored env file on
disk that the agent never reads back. That is what makes it safe to hand an autonomous agent.
Source of truth is Linux. The vault lives at ~/.codeamani/hazina inside WSL. The Windows
copy under C:\Users\info\.codeamani\hazina is archive-only — do not create new bindings
against it.
Official Documentation
| Topic | URL |
|---|---|
| Build an MCP server | https://modelcontextprotocol.io/docs/develop/build-server |
| Connect local (stdio) servers | https://modelcontextprotocol.io/docs/develop/connect-local-servers |
| TypeScript SDK | https://github.com/modelcontextprotocol/typescript-sdk |
| MCP in Claude Code | https://docs.claude.com/en/docs/claude-code/mcp |
| MCP in Cursor | https://cursor.com/docs/context/mcp |
| WSL filesystem / interop | https://learn.microsoft.com/en-us/windows/wsl/filesystems |
Hazina's own repo is private (codeAmani-Labs/hazina-mcp — the MCP layer split out from
the vault package); its docs ship in-tree at docs/INSTALL.md and docs/UBUNTU-GLOBAL.md.
How it is built
Hazina uses the standard MCP TypeScript SDK — McpServer + registerTool with Zod input
schemas, connected over StdioServerTransport:
npm install @modelcontextprotocol/sdk zodimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new McpServer({ name: 'hazina', version: VERSION });
server.registerTool(
'status',
{ description: 'Whether the vault is initialized…', inputSchema: {} },
async () => ({ content: [{ type: 'text', text: summary }] }),
);
await server.connect(new StdioServerTransport());Two rules follow from stdio transport and are non-negotiable:
- stdout is the protocol channel. All logging goes to stderr. A stray
console.logcorrupts the JSON-RPC stream and the client drops the server. - The client owns the lifecycle. It spawns the process; there is no port, no daemon.
Defense in depth — the value-free guard. Every tool result passes through a guarded()
wrapper before it leaves the process: it re-loads the catalog and asserts (assertNoValues)
that no secret value appears as a substring of the serialized payload, throwing rather than
returning if one does. So even a future buggy handler cannot leak a value across the agent
boundary. To avoid false rejections on short non-secret literals (a PORT, a public URL
fragment), the assertion only fires on catalog values at or above
MIN_ASSERTED_SECRET_LEN = 16 characters. That constant must stay in sync between the
vault package and the split-out codeAmani-Labs/hazina-mcp MCP layer — a length one side
treats as "too short to be a secret" the other must treat identically.
Install (WSL)
cd ~/projects/hazina
npm install
npm run install:global # -> ~/.local/bin/hazina and ~/.local/bin/hazina-mcp
hazina doctor # readiness: vault, key, DPAPI path, counts (names only)The hazina-mcp launcher is a bash wrapper that resolves its own symlinks, then:
- exports
HAZINA_HOMEdefaulting to$HOME/.codeamani/hazina - prepends the Windows PowerShell directory to
PATHifpowershell.exeis not already reachable — needed because the master key is DPAPI-wrapped and unwrapping shells out to Windows from inside WSL execs Node with thetsxloader againstsrc/mcp/server.ts
Because the launcher supplies its own HAZINA_HOME, client configs do not need an env
block — see the WSLENV note under Gotchas.
The 14 tools
All are value-free. Verified live against hazina 0.3.0.
| Tool | What it returns |
|---|---|
status | Vault initialized? entry + bound-project counts |
doctor | Readiness: HAZINA_HOME, vault/key presence, PowerShell DPAPI path |
list_refs | Every secret reference — path, type, tags, field names |
list_projects | Project names that have a binding |
get_binding | A project's env-to-ref mappings (literals shown as literals) |
describe_project | Each env var, its reference, and status (ok/missing/stale) |
wiring_summary | Fleet-wide graph of env-to-vault refs grouped by namespace |
propose_binding | Scans .env.example/.env.local, proposes a binding |
bind | Create/update a binding: ENV name to vault reference |
unbind | Remove one ENV name from a binding (does not delete the secret) |
inject | Resolve a binding, write the gitignored env file with real values |
audit | Cross-check catalog vs bindings: stale/unused entries |
import_from_os_env | Import from OS environment (Windows User/Machine/Process) |
push | Resolve a binding and push values to Vercel or Netlify |
There is no reveal / get_value tool. By design.
inject and push are the only tools that move plaintext, and both write it outward
(to a gitignored file, or to a host's env store) rather than returning it into the transcript.
Wiring it to clients
Where the client runs decides whether you need the bridge.
| Client | Runs on | Config file | Command |
|---|---|---|---|
| Grok Build (CLI) | inside WSL | ~/.grok/config.toml | /home/barnabas/.local/bin/hazina-mcp |
| Claude Code (in WSL) | inside WSL | ~/.claude.json | /home/barnabas/.local/bin/hazina-mcp |
| Cursor / Grok bot | Windows | ~/.cursor/mcp.json | wsl.exe bridge |
| Claude Code desktop | Windows | ~/.claude.json | wsl.exe bridge |
Windows clients — the wsl.exe bridge
{
"mcpServers": {
"hazina": {
"command": "wsl.exe",
"args": ["-d", "Ubuntu", "--", "/home/barnabas/.local/bin/hazina-mcp"]
}
}
}wsl.exe transparently proxies stdin/stdout, so the JSON-RPC stream survives the boundary
untouched. The same block works in ~/.cursor/mcp.json and in ~/.claude.json.
WSL-native clients
# ~/.grok/config.toml
[mcp_servers.hazina]
command = "/home/barnabas/.local/bin/hazina-mcp"claude mcp add --scope user hazina -- /home/barnabas/.local/bin/hazina-mcpVerifying the bridge
Do not trust the config — speak the protocol to it:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| wsl.exe -d Ubuntu -- /home/barnabas/.local/bin/hazina-mcpExpect "serverInfo":{"name":"hazina","version":"0.3.0"} followed by 14 tools.
Gotchas
envdoes not cross the WSL boundary. Anenvblock inmcp.jsonsets variables on the Windowswsl.exeprocess. Linux only inherits whatWSLENVforwards, so aHAZINA_HOMEset there is silently ignored. Rely on the launcher default, or be explicit:wsl.exe -d Ubuntu -- env HAZINA_HOME=/home/barnabas/.codeamani/hazina /home/barnabas/.local/bin/hazina-mcp- Git Bash rewrites Linux paths. Testing from Git Bash turns
/home/...intoC:/Program Files/Git/home/.... Prefix withMSYS_NO_PATHCONV=1. PowerShell and the agents themselves are unaffected. - DPAPI needs Windows reachable. The Linux vault's master key is DPAPI-wrapped; unwrapping
calls
powershell.exevia WSL interop. If interop is disabled,hazina doctorfails. - Distro name is load-bearing.
-d Ubuntumust matchwsl.exe -l -qexactly. - Never log to stdout in an stdio server — it corrupts the protocol stream.
Hazina vs the vendor MCPs
| Hazina | Cloudflare / Vercel / xAI | |
|---|---|---|
| Transport | local stdio (child process) | remote HTTP |
| Config key | command + args | url + Authorization header |
| Auth | filesystem + DPAPI-wrapped key | bearer API token / OAuth |
| Ownership | first-party, private | vendor-hosted |
| Failure mode | process will not spawn | 401 / network |
| Secrets exposure | none — value-free tools | token sits in the config |
Cloudflare's are already wired in ~/.cursor/mcp.json (mcp.cloudflare.com/mcp,
docs.mcp.cloudflare.com/mcp, bindings.mcp.cloudflare.com/mcp). Note the contrast: those
configs carry ${CLOUDFLARE_API_TOKEN} in a header — exactly the kind of sprawl Hazina exists
to eliminate. See cloudflare/, vercel/ and xai/ for the vendor-side guides.
codeAmani notes
- Zero-exposure is the product. Never add a tool that returns a secret value. If an agent
needs a value, it needs
injectwriting a gitignored file — not a tool result in a transcript that gets logged, summarized and cached. - One vault, Linux SoT.
~/.codeamani/hazinain WSL. The Windows path is archived; new bindings against it will drift. - Secrets stay server-side. Hazina is local-only — never deploy the MCP server to Vercel or expose it over HTTP. It has no authentication layer because it never needed one.
- Audit before shipping.
auditcatches stale refs;wiring_summaryshows which project pulls which namespace. Run both before a release touches env config. - Vault data lives outside the repo and
audit.logrecords tool calls — treat it as sensitive-but-value-free, and keep it out of git.