@eddalabs/karbonity-mcp
v0.2.3
Published
Karbonity MCP server (stdio) with optional local Midnight agent wallet — connect Claude Code / Claude Desktop to the Karbonity certificate marketplace
Readme
karbonity-mcp — connect AI agents to Karbonity
Two packages, one per Midnight network. Everything below uses the Preview names; for Preprod substitute them as follows (both can be installed side by side):
| | Preview | Preprod |
|---|---|---|
| npm package | @eddalabs/karbonity-mcp | @eddalabs/karbonity-mcp-preprod |
| command | karbonity-mcp | karbonity-mcp-preprod |
| server name in Claude | karbonity | karbonity-preprod |
| config dir (login, wallet) | ~/.karbonity | ~/.karbonity-preprod |
| faucet | https://faucet.preview.midnight.network/ | https://faucet.preprod.midnight.network/ |
| agent wallet chain access | public Preview endpoints | your own Blockfrost Midnight Preprod project id |
Preprod is only served through Blockfrost, which authenticates with a token in the URL, so the package can't ship one. Only the optional agent wallet (section C) needs it; browsing, the bot tools and draft links work without it. Give it to the server in its environment, or store it once:
claude mcp add karbonity-preprod -s local -e KARBONITY_BLOCKFROST_PROJECT_ID='preprod…' \
-e KARBONITY_WALLET_PASSPHRASE='…' -- karbonity-mcp-preprod serve
# or: karbonity-mcp-preprod init --blockfrost-project-id preprod… (stored in ~/.karbonity-preprod/config.json, 0600)karbonity-mcp-preprod doctor shows which network, backend and chain endpoints it uses.
Karbonity exposes one MCP tool catalog (packages/mcp-core) over two transports.
Pick the one that matches where your agent runs:
| You use… | Transport | Setup |
|---|---|---|
| Claude.ai (web/desktop/mobile chat), Cowork | Remote HTTP — https://<deployment>.convex.site/mcp | A. Remote connector |
| Claude Code or Claude Desktop on your machine | Local stdio — karbonity-mcp serve | B. Local server |
Both give the same 20 tools (browse the marketplace, your certificates, create/list/price/burn via the bot, and buy/redeem/withdraw/swap). The difference is wallet actions:
- Remote:
buy/redeem/withdraw_seller_funds/swap_nightreturn a URL. Open it, review, and sign in the web UI with your own wallet (Lace or passkey). The server never holds a key. - Local + agent wallet: the same tools execute on your machine with a dedicated, funded agent wallet. See C.
Amounts everywhere are decimal strings in the smallest Night unit (2000000 = 2 Night).
One-time backend setup (operator)
Do this once per deployment. Already done for the dev deployment animated-spoonbill-938.
- Clerk Dashboard → OAuth applications → Create
- Client type Public (PKCE, no secret).
- Redirect URIs:
https://claude.ai/api/mcp/auth_callbackhttp://127.0.0.1:44831/callbackhttp://localhost/callback - Leave Generate access tokens as JWTs on.
- Optional: enable Dynamic client registration so Claude.ai can add the connector by URL alone.
- Convex environment variables (
npx convex env set …inpackages/backend):CONTRACT_ADDRESS— the mini-private-buyer contract (same asVITE_CONTRACT_ADDRESS)SITE_URL— web app origin, used for draft links (e.g.http://localhost:5173)MCP_OAUTH_ISSUER— optional; defaults toCLERK_FRONTEND_API_URLMCP_OAUTH_AUDIENCE— any value enables agent auth;offdisables it
- Deploy:
npx convex dev --once(use Node 22 — Node 23 segfaults the Convex CLI).
How auth works: Clerk issues OAuth access tokens as JWTs signed by the same JWKS as your web
session. convex/auth.config.js trusts them with a Custom JWT provider, so ctx.auth sees
the same Clerk user id and every existing Convex function works unchanged for agents.
A. Remote connector (Claude.ai)
- Claude.ai → Settings → Connectors → Add custom connector.
- URL:
https://animated-spoonbill-938.convex.site/mcp - If Dynamic client registration is off in Clerk: open Advanced settings and paste the
OAuth app's Client ID (
GmOu49U0Yux4yxUEon dev). No secret. - Click Connect → sign in with your Karbonity (Clerk) account → done.
Claude Code can also use the remote server:
claude mcp add --transport http karbonity-remote https://animated-spoonbill-938.convex.site/mcp --client-id GmOu49U0Yux4yxUE
B. Local server (Claude Code / Desktop) — Linux or WSL
Requires Node ≥ 20 on Linux (on Windows, inside WSL). Not supported natively on Windows.
1. Install
npm install -g @eddalabs/karbonity-mcp
karbonity-mcp doctor # shows backend, login, wallet and proof-server statusThe production backend is built in — no configuration step. (karbonity-mcp init --convex-site … --contract …
only if you run your own deployment.)
2. Log in
karbonity-mcp login # opens a browser URL; sign in with your Karbonity accountThe token is stored in ~/.karbonity/auth.json (mode 0600) and refreshed automatically.
karbonity-mcp logout forgets it.
3. Register with Claude
Claude Code (run inside the project you want it in):
claude mcp add karbonity -s local -- karbonity-mcp serveClaude Desktop on Linux — paste into claude_desktop_config.json:
{ "mcpServers": { "karbonity": { "command": "karbonity-mcp", "args": ["serve"] } } }(karbonity-mcp desktop-config prints this for you.)
Per-project, zero install — drop a .mcp.json at the repo root and everyone who opens it gets the
server (Claude Code asks each user to approve the project's servers on first use):
{
"mcpServers": {
"karbonity": {
"command": "npx",
"args": ["-y", "@eddalabs/karbonity-mcp", "serve"],
"env": { "KARBONITY_WALLET_PASSPHRASE": "${KARBONITY_WALLET_PASSPHRASE:-}" }
}
}
}Claude Code expands ${VAR} and ${VAR:-default} in .mcp.json, so the file references the passphrase
without containing it — each user exports KARBONITY_WALLET_PASSPHRASE in their own shell (or leaves it
unset and gets the draft flow). Keep the :- default: without it, an unset variable is passed through as
the literal text ${KARBONITY_WALLET_PASSPHRASE}. Never put the passphrase itself in a committed file.
No global install? Any client can run it straight from npm instead:
{ "mcpServers": { "karbonity": { "command": "npx", "args": ["-y", "@eddalabs/karbonity-mcp", "serve"] } } }The first launch downloads ~66 MB, which can trip the client's startup timeout; after that it is cached. A global install starts instantly, so it is the better default.
Claude Desktop on Windows, code in WSL — Desktop runs on Windows, so it must start the server inside
your distro through wsl.exe. Run in WSL:
karbonity-mcp desktop-config --windowsand paste the output into %APPDATA%\Claude\claude_desktop_config.json, then restart Desktop. It looks like:
{ "mcpServers": { "karbonity": { "command": "wsl.exe",
"args": ["-d", "Ubuntu", "--", "/home/<you>/.nvm/versions/node/v22.x/bin/node",
"/home/<you>/.nvm/versions/node/v22.x/lib/node_modules/@eddalabs/karbonity-mcp/dist/cli.js", "serve"] } } }Windows is the one place absolute paths are unavoidable: wsl.exe starts no login shell, so npm's global
bin (and nvm) are not on PATH — karbonity-mcp alone would not be found. desktop-config --windows
fills the paths in for you. The browser sign-in from login opens on Windows and reaches WSL through
WSL2's localhost forwarding.
Then /mcp in Claude Code (or restart Desktop). You should see 20 karbonity tools.
Try: "list my certificates", "what's for sale?", "create a Solar certificate and list it for 2 Night".
Never run karbonity-mcp serve by hand for normal use — Claude launches it and talks to it over stdin/stdout.
From source (contributors)
pnpm --filter @eddalabs/karbonity-mcp build # → packages/mcp-cli/dist (bundle + 65 MB prover keys)
claude mcp add karbonity -s local -- node $PWD/packages/mcp-cli/dist/cli.js servepnpm dev runs the TypeScript directly through tsx.
C. Optional: local agent wallet
Without this, wallet actions return draft URLs (same as the remote connector). With it, the local server signs and proves on your machine.
Why a separate wallet: your passkey wallet's seed comes from WebAuthn PRF and cannot run headless; Lace lives in the browser. So the agent gets its own identity. Fund it only with what you're willing to let an agent spend; sweep it to revoke.
karbonity-mcp wallet init # prints the 24-word phrase ONCE; asks for a passphrase
karbonity-mcp wallet show # addresses + seller idThe mnemonic is encrypted with your passphrase (scrypt + AES-GCM) in ~/.karbonity/wallet.enc.
Fund the unshielded address from
wallet showat https://faucet.preview.midnight.network/Run a proof server on
127.0.0.1:6300:karbonity-mcp proof-server start --max-k 17 # Docker required karbonity-mcp proof-server status | logs | stopWhy local: generating a proof means giving the prover your witness (zero-knowledge protects verifiers, not the prover). A remote proof server would therefore see your
secretNonceon every transaction. Keep it on your machine unless the server is inside your own trust domain.About
--max-k. The image fetches PLONK parameters fromsrs.midnight.networkautomatically — it prefetches k=10..15 at startup and pulls higher k on demand, which stalls the first proof that needs one.buyandredeemare fine on the defaults; shield/unshield need k=16 and 17 (≈13 MB and ≈25 MB).--max-k 17downloads them up front into~/.karbonity/zk-params, which is then bind-mounted — raise the number if other circuits need more (each k doubles: k=18 ≈ 50 MB, k=19 ≈ 100 MB). Files already present are skipped, so re-running is cheap.Already have a parameter cache?
--params-dir /path/to/zk-paramsmounts it as-is, no downloads. Running your own server elsewhere?karbonity-mcp init --proof-server URL.Give the server the passphrase — it cannot prompt, because stdin is the MCP channel:
claude mcp remove karbonity -s local claude mcp add karbonity -s local -e KARBONITY_WALLET_PASSPHRASE='…' -- karbonity-mcp serve # Claude Desktop (incl. Windows+WSL): KARBONITY_WALLET_PASSPHRASE='…' karbonity-mcp desktop-config --windows --with-passphrase(The passphrase is then stored in plain text in that config file. If you'd rather not, leave it unset and use draft URLs.)
/mcp→ reconnect. Awallet_infotool appears;buy,redeem,withdraw_seller_funds,swap_nightnow execute locally.
The wallet's private state (secretNonce + purchases) is stored encrypted in Convex's
walletVaults table exactly like browser wallets, keyed from the agent seed — Convex cannot
link the row to you or to the wallet.
Troubleshooting
| Symptom | Fix |
|---|---|
| karbonity-mcp: command not found | npm install -g @eddalabs/karbonity-mcp with Node ≥ 20; check npm prefix -g/bin is on PATH. |
| Tool returns "Not logged in" | karbonity-mcp login. |
| whoami → authenticated: false or a "No auth provider" error | Backend setup step 2/3 not applied, or the token comes from a different Clerk instance than CLERK_FRONTEND_API_URL. |
| Claude Code shows karbonity ✘ Failed to connect | Use karbonity-mcp desktop-config output (absolute paths) instead of a bare command; run that exact command from / — it must print nothing on stdout until it receives JSON. |
| Server says "wallet found but KARBONITY_WALLET_PASSPHRASE is not set" | Expected without step C.3; wallet tools fall back to drafts. |
| Server says "could not unlock the agent wallet" | Wrong passphrase (or a ${VAR} that the client never expanded). The server keeps running — only wallet tools degrade to drafts. |
| Login page says redirect URI mismatch | Add http://127.0.0.1:44831/callback to the Clerk OAuth app. |
| Windows Desktop: server never connects | Run the wsl.exe … line from PowerShell; if it prints an error, fix the distro name or path. The proof server (pnpm ps) must also run inside WSL. |
Commands
karbonity-mcp login | logout | whoami | doctor | version
karbonity-mcp desktop-config [--windows] [--with-passphrase]
karbonity-mcp wallet init | import <words…> | show
karbonity-mcp serve # MCP over stdio (launched by Claude)
karbonity-mcp init [--convex-site URL] [--site URL] [--contract HEX] [--proof-server URL] [--issuer URL] [--client-id ID]
[--blockfrost-project-id ID] [--indexer URL] [--indexer-ws URL] [--node URL]Releasing (maintainers)
Published as @eddalabs/karbonity-mcp (Preview)
and @eddalabs/karbonity-mcp-preprod (Preprod)
under the eddalabs npm org, from the same source. The release workflow renames the package and
its command for Preprod and bakes the network in (KARBONITY_NETWORK → scripts/build.mjs). packages/mcp-core stays private: true — esbuild inlines it into
dist/cli.js, so it is never resolved from the registry.
npm credentials (one-time). Publishing needs a token whose 2FA requirement is satisfied:
either a granular access token created with Bypass two-factor authentication checked
(Access Tokens → Generate New Token → Granular; permissions Read and write on the
@eddalabs scope — granting access to the Organization is not enough, it only covers org
settings), or a normal granular token combined with account 2FA set to "Authorization only"
(Account settings → Two-factor authentication). Install it with
npm config set //registry.npmjs.org/:_authToken npm_xxx — do not pass it to --otp,
which expects a 6-digit code.
Release from CI (preferred). Bump version in packages/mcp-cli/package.json on main
and promote it to the network branch. That's the whole release: when the merge lands on
preview or preprod, .github/workflows/release-mcp-cli.yml asks npm whether that version of
the network's package exists, and if not it builds, publishes, and tags the commit as a record:
| Branch | Package | Tag it creates |
|---|---|---|
| preview | @eddalabs/karbonity-mcp | mcp-cli-v<version> |
| preprod | @eddalabs/karbonity-mcp-preprod | mcp-cli-preprod-v<version> |
Merges that don't bump the version, and re-runs, skip publishing. Nobody pushes tags by hand. Run workflow on a network branch (Actions tab) does the same check on demand, e.g. to retry after fixing a variable.
The deployment baked into the build comes from the repository variables the web deploy already
uses, with the network's prefix <P>_ (PREPROD_ or PREVIEW_):
| Baked value | Repository variable |
|---|---|
| Convex site URL | <P>_VITE_CONVEX_SITE_URL |
| contract address | <P>_VITE_CONTRACT_ADDRESS |
| OAuth client id | <P>_VITE_MCP_OAUTH_CLIENT_ID |
| OAuth issuer | decoded from <P>_VITE_CLERK_PUBLISHABLE_KEY (Clerk's Frontend API) |
| web app URL (draft links) | <P>_SITE_URL, the only variable of its own |
<P>_KARBONITY_<NAME> (e.g. PREPROD_KARBONITY_OAUTH_ISSUER) overrides any of them, and Preview
also still reads the older unprefixed KARBONITY_* variables.
The workflow fails if any value above is missing, and publishes through npm Trusted Publishing:
set per package on npmjs.com (package → Settings → Trusted publisher → GitHub Actions,
eddalabs / karbonity-gameplay / release-mcp-cli.yml), with no token stored anywhere. npm
only offers that on a package that already exists, so a brand-new package's first version is
published by hand (below), then Trusted Publishing is configured and CI takes over.
Each network's Clerk OAuth app needs the CLI's login redirect http://127.0.0.1:44831/callback.
Release manually (first version of a new package, or without CI). From a clone of this repo, on the network branch's commit. For Preprod, rename the package and give the build the deployment values the workflow would (same sources as the table above):
git fetch origin preprod && git checkout --detach origin/preprod
pnpm install
cd packages/mcp-cli
npm pkg delete bin && npm pkg set name=@eddalabs/karbonity-mcp-preprod bin.karbonity-mcp-preprod=dist/cli.js
export KARBONITY_NETWORK=preprod KARBONITY_CONVEX_SITE_URL=… KARBONITY_SITE_URL=… \
KARBONITY_CONTRACT_ADDRESS=… KARBONITY_OAUTH_ISSUER=… KARBONITY_OAUTH_CLIENT_ID=…
npm login # once; npm asks for your 2FA code on publish
npm publish --access public
git checkout package.jsonFor Preview, skip the rename and KARBONITY_NETWORK (the repo already names the Preview package):
pnpm --filter @eddalabs/karbonity-mcp build
cd packages/mcp-cli
npm pack # inspect: dist/cli.js + dist/contract/**/keys, no src/
npm publish --access public # ~66 MB; the registry document takes a few minutes to propagateNotes: prepack runs the build, so npm publish never ships a stale bundle. Right after a first
publish npm view/npm install can 404 for a few minutes while the CDN catches up — npm search
and npm access list packages @eddalabs confirm it landed.
