Kvant

MCP server

Post-quantum tools for your AI agents

Six tools on one local server: encrypt files, send sealed messages, read Quantum Seals and sign x402 payments, from Claude, Cursor or any MCP client.

6Post-quantum tools over stdio
Install · next
$ npx -y @kvant/mcp

Install · next

One command, next.

The server is built and tested; the npm release is next. It ships under the @kvant scope, which only Kvant can publish to, so this command can never run someone else’s upload. Node.js 20 or later runs it over stdio.

Claude Desktop

claude_desktop_config.json
{
"mcpServers": {
"kvant": {
"command": "npx",
"args": ["-y", "@kvant/mcp"]
}
}
}

This config runs once the package is on npm: drop it into your config and restart the client, and Kvant tools appear in the tools picker.

Cursor

~/.cursor/mcp.json
{
"mcpServers": {
"kvant": {
"command": "npx",
"args": ["-y", "@kvant/mcp"]
}
}
}

Same shape, same server. Works with any client that speaks Model Context Protocol over stdio.

Tools

Six primitives.

ToolDescriptionInputsOutputs
kvant_encrypt_fileEncrypt bytes under ML-KEM-768 + X25519 + ML-DSA-65.bytes_base64, recipient_pubkey_base64, signer?: keyfile | signature + addressblob_base64, seal_png_base64, signer_pubkey_base64
kvant_decrypt_fileDecrypt a .kvant blob with a keyfile or wallet signature.blob_base64, (keyfile_json + passphrase | signature_hex + address), expected_signer_pubkey_base64 (required unless self-signed)plaintext_base64, signer_pubkey_base64, filename
kvant_send_messageWrap text in a shareable encrypted URL, optionally signed by your wallet.text, recipient_pubkey_base64?, signer?: keyfile | signature + addressurl, mode
kvant_decrypt_messageRecover plaintext + signer from a Kvant message URL.url, (keyfile_json + passphrase | signature_hex + address)?, expected_signer_pubkey_base64?text, signer_pubkey_base64
kvant_verify_sealRead the unsigned fields of a Quantum Seal PNG. Given the envelope, check that its SHA-256 matches the Seal file_hash.png_base64, blob_base64?valid, metadata { version, timestamp, algo, file_hash, wallet?, x402?, anchor? }, file_hash_matches?
kvant_sign_x402Sign an x402 payload with ML-DSA-65 under context pqenv-v1-x402.payload_json (JSON object), (keyfile_json + passphrase | signature_hex + address)signature_base64, signer_pubkey_base64

Every tool runs locally over stdio. No network I/O. No telemetry. Keyfiles are unwrapped in-process with Argon2id and never persisted. A wallet signature is root key material: pass it over local stdio only, never a networked transport.

x402 signatures

What kvant_sign_x402 signs.

The tool signs the UTF-8 bytes of the payload’s canonical form with ML-DSA-65, under the context pqenv-v1-x402. Anyone holding the payload, signature_base64 and signer_pubkey_base64 can check which key signed, with any FIPS 204 implementation and no Kvant code.

  • Object keys sorted in JavaScript default string order (UTF-16 code units), at every level.
  • No whitespace. Arrays keep their order.
  • Strings, numbers, booleans and null written exactly as JSON.stringify writes them.
  • The ML-DSA-65 public key is bytes 1216 to 3168 of the decoded signer_pubkey_base64, after the ML-KEM-768 and X25519 parts.
verify-x402.ts
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa' // 0.4.x argument order
// payload {"b":1,"a":{"d":[true,null],"c":2.5}} is signed as:
const canonical = '{"a":{"c":2.5,"d":[true,null]},"b":1}'
const hybridKey = Buffer.from(signer_pubkey_base64, 'base64')
const mldsaKey = hybridKey.subarray(1216, 3168)
const valid = ml_dsa65.verify(
mldsaKey,
new TextEncoder().encode(canonical),
Buffer.from(signature_base64, 'base64'),
new TextEncoder().encode('pqenv-v1-x402'),
)

Wallet identity

Your wallet is the key.

An agent signs one fixed message with its EOA wallet and derives the same hybrid keypair the browser does. Encrypt at /app, decrypt from the agent: no keyfile export, no copied secrets. Same wallet, same keys, same files, as long as the wallet signs deterministically. EOA only: smart-contract wallets are rejected at derivation. MPC and threshold signers return a different signature every time; the browser catches them by asking for two signatures the first time an address derives keys on a device. The server runs the same check when you pass a second signature as signature_hex_repeat the first time you use a wallet, and it refuses a signature that does not recover to the address you name. Sign the Kvant message only in your own agent or on kvant.world: anyone who gets the signature can open your files. For a durable copy, /app wraps the same keypair as a passphrase-protected keyfile (Back up keys).

agent.ts
import { privateKeyToAccount } from 'viem/accounts'
const SIGN_MESSAGE = [
'Kvant keypair derivation v1',
'',
'This signature derives your personal post-quantum encryption keypair.',
'It grants no permissions and costs no gas.',
'',
'Domain: kvant.world',
].join('\n')
// An EOA with a local private key signs with RFC 6979: the bytes repeat.
// MPC and threshold signers produce a different valid signature each time,
// so pass a second one the first time: the server refuses the call if they differ.
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY)
const signature_hex = await account.signMessage({ message: SIGN_MESSAGE })
const signature_hex_repeat = await account.signMessage({ message: SIGN_MESSAGE })
// The server recovers the signer and refuses a signature that is not from address.
// Encrypting tools take the same fields with a signer_ prefix.
await mcp.call('kvant_decrypt_file', {
blob_base64, // encrypted to this wallet in /app
signature_hex,
address: account.address,
signature_hex_repeat,
})