@rougechain/mcp-server
Advanced tools
+10
-1
@@ -8,5 +8,14 @@ #!/usr/bin/env node | ||
| * | ||
| * READ tools work with no configuration. | ||
| * WRITE tools (transfer, swap, mint, post, …) sign transactions with ML-DSA-65 | ||
| * and are only registered when a wallet is provided via env: | ||
| * | ||
| * ROUGECHAIN_MNEMONIC="word1 word2 … word24" (preferred — BIP-39 seed) | ||
| * …or… | ||
| * ROUGECHAIN_PRIVATE_KEY=<hex> ROUGECHAIN_PUBLIC_KEY=<hex> | ||
| * | ||
| * Usage: | ||
| * ROUGECHAIN_URL=https://api.rougechain.io npx @rougechain/mcp-server | ||
| * ROUGECHAIN_URL=https://api.rougechain.io npx @rougechain/mcp-server # read-only | ||
| * ROUGECHAIN_MNEMONIC="…" npx @rougechain/mcp-server # read + write | ||
| */ | ||
| export {}; |
+244
-3
@@ -8,4 +8,13 @@ #!/usr/bin/env node | ||
| * | ||
| * READ tools work with no configuration. | ||
| * WRITE tools (transfer, swap, mint, post, …) sign transactions with ML-DSA-65 | ||
| * and are only registered when a wallet is provided via env: | ||
| * | ||
| * ROUGECHAIN_MNEMONIC="word1 word2 … word24" (preferred — BIP-39 seed) | ||
| * …or… | ||
| * ROUGECHAIN_PRIVATE_KEY=<hex> ROUGECHAIN_PUBLIC_KEY=<hex> | ||
| * | ||
| * Usage: | ||
| * ROUGECHAIN_URL=https://api.rougechain.io npx @rougechain/mcp-server | ||
| * ROUGECHAIN_URL=https://api.rougechain.io npx @rougechain/mcp-server # read-only | ||
| * ROUGECHAIN_MNEMONIC="…" npx @rougechain/mcp-server # read + write | ||
| */ | ||
@@ -15,2 +24,3 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
| import { RougeChain, Wallet } from "@rougechain/sdk"; | ||
| // ─── Configuration ──────────────────────────────────────────────────────────── | ||
@@ -22,3 +32,3 @@ // NOTE: this must be the API host (api.rougechain.io), NOT the frontend host | ||
| const API_KEY = process.env.ROUGECHAIN_API_KEY || ""; | ||
| // ─── HTTP helpers ───────────────────────────────────────────────────────────── | ||
| // ─── HTTP helpers (read side) ────────────────────────────────────────────────── | ||
| const headers = { | ||
@@ -40,6 +50,42 @@ "Content-Type": "application/json", | ||
| } | ||
| // ─── SDK client (write side) ──────────────────────────────────────────────────── | ||
| // The SDK owns all ML-DSA-65 signing + transaction serialization; the MCP server | ||
| // never re-implements crypto. `rc` points at the same API base as the read helpers. | ||
| const rc = new RougeChain(API, API_KEY ? { apiKey: API_KEY } : {}); | ||
| /** | ||
| * Load a signing wallet from the environment, if one was provided. | ||
| * Returns null when the server should run read-only. | ||
| */ | ||
| function loadWallet() { | ||
| const mnemonic = process.env.ROUGECHAIN_MNEMONIC?.trim(); | ||
| const priv = process.env.ROUGECHAIN_PRIVATE_KEY?.trim(); | ||
| const pub = process.env.ROUGECHAIN_PUBLIC_KEY?.trim(); | ||
| try { | ||
| if (mnemonic) | ||
| return Wallet.fromMnemonic(mnemonic); | ||
| if (priv && pub) | ||
| return Wallet.fromKeys(pub, priv); | ||
| } | ||
| catch (e) { | ||
| console.error("[rougechain-mcp] Failed to load wallet from env — write tools disabled:", e instanceof Error ? e.message : e); | ||
| return null; | ||
| } | ||
| return null; | ||
| } | ||
| const wallet = loadWallet(); | ||
| const signer = wallet; | ||
| // Uniform JSON text result + error envelope so a failed tx never crashes the tool. | ||
| function ok(data) { | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| function fail(message) { | ||
| return { | ||
| isError: true, | ||
| content: [{ type: "text", text: JSON.stringify({ success: false, error: message }, null, 2) }], | ||
| }; | ||
| } | ||
| // ─── MCP Server ─────────────────────────────────────────────────────────────── | ||
| const server = new McpServer({ | ||
| name: "rougechain", | ||
| version: "1.0.0", | ||
| version: "1.1.0", | ||
| }); | ||
@@ -241,2 +287,191 @@ // ══════════════════════════════════════════════════════════════════════════════ | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // WALLET TOOLS — always available (no signer required) | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| server.tool("generate_wallet", "Generate a brand-new post-quantum (ML-DSA-65) wallet: 24-word BIP-39 mnemonic, " + | ||
| "public key, and rouge1… address. The keys are NOT saved anywhere — copy the " + | ||
| "mnemonic somewhere safe, then set ROUGECHAIN_MNEMONIC to enable transacting.", {}, async () => { | ||
| const w = Wallet.generate(); | ||
| return ok({ | ||
| mnemonic: w.mnemonic, | ||
| publicKey: w.publicKey, | ||
| address: await w.address(), | ||
| warning: "Store the mnemonic securely and never share it. Anyone with it controls this wallet. " + | ||
| "This server did not persist it.", | ||
| }); | ||
| }); | ||
| server.tool("wallet_info", "Report the signing wallet this server is configured with (public key, address, " + | ||
| "live balance) and whether write/transaction tools are enabled.", {}, async () => { | ||
| if (!signer) { | ||
| return ok({ | ||
| signer_configured: false, | ||
| write_tools_enabled: false, | ||
| hint: "Set ROUGECHAIN_MNEMONIC (or ROUGECHAIN_PRIVATE_KEY + ROUGECHAIN_PUBLIC_KEY) " + | ||
| "in the server env to enable transacting.", | ||
| }); | ||
| } | ||
| const address = await wallet.address(); | ||
| let balance = undefined; | ||
| try { | ||
| balance = await rc.getBalance(signer.publicKey); | ||
| } | ||
| catch { | ||
| /* balance is best-effort */ | ||
| } | ||
| return ok({ | ||
| signer_configured: true, | ||
| write_tools_enabled: true, | ||
| publicKey: signer.publicKey, | ||
| address, | ||
| balance, | ||
| }); | ||
| }); | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // WRITE / TRANSACTION TOOLS — registered only when a signing wallet is configured. | ||
| // Every transaction is signed locally with ML-DSA-65 via @rougechain/sdk. | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| async function tx(fn) { | ||
| try { | ||
| return ok(await fn()); | ||
| } | ||
| catch (e) { | ||
| return fail(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| if (signer) { | ||
| const w = signer; | ||
| // ── Core transfers, staking, faucet ───────────────────────────────────────── | ||
| server.tool("send_transaction", "Send XRGE or a custom token from the configured wallet to another address.", { | ||
| to: z.string().describe("Recipient address (rouge1…) or public key hex"), | ||
| amount: z.number().positive().describe("Amount to send"), | ||
| token: z.string().optional().describe("Token symbol (omit for native XRGE)"), | ||
| fee: z.number().optional().describe("Network fee (default 1)"), | ||
| }, async ({ to, amount, token, fee }) => tx(() => rc.transfer(w, { to, amount, token, fee }))); | ||
| server.tool("burn_tokens", "Permanently burn XRGE or a token from the configured wallet (sends to the burn address).", { | ||
| amount: z.number().positive().describe("Amount to burn"), | ||
| token: z.string().optional().describe("Token symbol (default XRGE)"), | ||
| fee: z.number().optional().describe("Network fee (default 1)"), | ||
| }, async ({ amount, token, fee }) => tx(() => rc.burn(w, amount, fee ?? 1, token ?? "XRGE"))); | ||
| server.tool("stake", "Stake XRGE from the configured wallet to participate in consensus / earn rewards.", { | ||
| amount: z.number().positive().describe("Amount of XRGE to stake"), | ||
| fee: z.number().optional().describe("Network fee (default 1)"), | ||
| }, async ({ amount, fee }) => tx(() => rc.stake(w, { amount, fee }))); | ||
| server.tool("unstake", "Unstake previously-staked XRGE back to the configured wallet.", { | ||
| amount: z.number().positive().describe("Amount of XRGE to unstake"), | ||
| fee: z.number().optional().describe("Network fee (default 1)"), | ||
| }, async ({ amount, fee }) => tx(() => rc.unstake(w, { amount, fee }))); | ||
| server.tool("request_faucet", "Request testnet XRGE from the faucet for the configured wallet (testnet only).", {}, async () => tx(() => rc.faucet(w))); | ||
| // ── Token issuance & administration ────────────────────────────────────────── | ||
| server.tool("create_token", "Create a new custom token on RougeChain (the configured wallet becomes the creator).", { | ||
| name: z.string().describe("Human-readable token name"), | ||
| symbol: z.string().describe("Ticker symbol, e.g. MYTOKEN"), | ||
| totalSupply: z.number().positive().describe("Initial total supply"), | ||
| image: z.string().optional().describe("Logo URL or data URI"), | ||
| fee: z.number().optional().describe("Creation fee (default 10)"), | ||
| }, async ({ name, symbol, totalSupply, image, fee }) => tx(() => rc.createToken(w, { name, symbol, totalSupply, image, fee }))); | ||
| server.tool("mint_tokens", "Mint additional supply of a mintable token (configured wallet must be the creator).", { | ||
| symbol: z.string().describe("Token symbol to mint"), | ||
| amount: z.number().positive().describe("Amount to mint"), | ||
| fee: z.number().optional().describe("Network fee (default 1)"), | ||
| }, async ({ symbol, amount, fee }) => tx(() => rc.mintTokens(w, { symbol, amount, fee }))); | ||
| server.tool("update_token_metadata", "Update the metadata (logo, description, links) of a token you created.", { | ||
| symbol: z.string().describe("Token symbol"), | ||
| image: z.string().optional().describe("Logo URL or data URI"), | ||
| description: z.string().optional(), | ||
| website: z.string().optional(), | ||
| twitter: z.string().optional(), | ||
| discord: z.string().optional(), | ||
| }, async ({ symbol, image, description, website, twitter, discord }) => tx(() => rc.updateTokenMetadata(w, { symbol, image, description, website, twitter, discord }))); | ||
| server.tool("claim_token_metadata", "Claim metadata authority for a token (creator verification).", { symbol: z.string().describe("Token symbol") }, async ({ symbol }) => tx(() => rc.claimTokenMetadata(w, symbol))); | ||
| // ── DEX / AMM ──────────────────────────────────────────────────────────────── | ||
| server.tool("swap", "Execute a token swap on the AMM DEX from the configured wallet. " + | ||
| "Use get_swap_quote first to size min_amount_out for slippage protection.", { | ||
| tokenIn: z.string().describe("Symbol of token to sell"), | ||
| tokenOut: z.string().describe("Symbol of token to buy"), | ||
| amountIn: z.number().positive().describe("Amount of tokenIn to sell"), | ||
| minAmountOut: z.number().nonnegative().describe("Minimum acceptable tokenOut (slippage guard)"), | ||
| }, async ({ tokenIn, tokenOut, amountIn, minAmountOut }) => tx(() => rc.dex.swap(w, { tokenIn, tokenOut, amountIn, minAmountOut }))); | ||
| server.tool("create_pool", "Create a new liquidity pool for a token pair with an initial deposit.", { | ||
| tokenA: z.string().describe("First token symbol"), | ||
| tokenB: z.string().describe("Second token symbol"), | ||
| amountA: z.number().positive().describe("Initial amount of tokenA"), | ||
| amountB: z.number().positive().describe("Initial amount of tokenB"), | ||
| }, async ({ tokenA, tokenB, amountA, amountB }) => tx(() => rc.dex.createPool(w, { tokenA, tokenB, amountA, amountB }))); | ||
| server.tool("add_liquidity", "Add liquidity to an existing pool and receive LP tokens.", { | ||
| poolId: z.string().describe("Pool ID"), | ||
| amountA: z.number().positive().describe("Amount of tokenA to add"), | ||
| amountB: z.number().positive().describe("Amount of tokenB to add"), | ||
| }, async ({ poolId, amountA, amountB }) => tx(() => rc.dex.addLiquidity(w, { poolId, amountA, amountB }))); | ||
| server.tool("remove_liquidity", "Withdraw liquidity from a pool by burning LP tokens.", { | ||
| poolId: z.string().describe("Pool ID"), | ||
| lpAmount: z.number().positive().describe("Amount of LP tokens to burn"), | ||
| }, async ({ poolId, lpAmount }) => tx(() => rc.dex.removeLiquidity(w, { poolId, lpAmount }))); | ||
| // ── NFTs ────────────────────────────────────────────────────────────────────── | ||
| server.tool("nft_create_collection", "Create a new NFT collection (configured wallet becomes the owner/creator).", { | ||
| symbol: z.string().describe("Collection symbol"), | ||
| name: z.string().describe("Collection name"), | ||
| maxSupply: z.number().optional().describe("Max mintable supply"), | ||
| royaltyBps: z.number().optional().describe("Creator royalty in basis points (100 = 1%)"), | ||
| image: z.string().optional().describe("Cover image URL or data URI"), | ||
| description: z.string().optional(), | ||
| publicMint: z.boolean().optional().describe("Allow anyone to mint"), | ||
| mintPrice: z.number().optional().describe("Price per mint if publicMint"), | ||
| }, async (a) => tx(() => rc.nft.createCollection(w, a))); | ||
| server.tool("nft_mint", "Mint a single NFT into a collection.", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| name: z.string().describe("NFT name"), | ||
| metadataUri: z.string().optional().describe("Metadata URI (ipfs/https/data)"), | ||
| attributes: z.record(z.unknown()).optional().describe("Trait attributes"), | ||
| }, async ({ collectionId, name, metadataUri, attributes }) => tx(() => rc.nft.mint(w, { collectionId, name, metadataUri, attributes }))); | ||
| server.tool("nft_batch_mint", "Mint many NFTs into a collection in one transaction.", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| names: z.array(z.string()).describe("Names for each NFT (defines batch size)"), | ||
| uris: z.array(z.string()).optional().describe("Metadata URI per NFT (parallel to names)"), | ||
| }, async ({ collectionId, names, uris }) => tx(() => rc.nft.batchMint(w, { collectionId, names, uris }))); | ||
| server.tool("nft_transfer", "Transfer an NFT to another wallet, optionally recording a sale price.", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| tokenId: z.number().int().describe("Token ID within the collection"), | ||
| to: z.string().describe("Recipient address or public key"), | ||
| salePrice: z.number().optional().describe("Sale price to record (for royalties)"), | ||
| }, async ({ collectionId, tokenId, to, salePrice }) => tx(() => rc.nft.transfer(w, { collectionId, tokenId, to, salePrice }))); | ||
| server.tool("nft_burn", "Permanently burn an NFT you own.", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| tokenId: z.number().int().describe("Token ID"), | ||
| }, async ({ collectionId, tokenId }) => tx(() => rc.nft.burn(w, { collectionId, tokenId }))); | ||
| server.tool("nft_lock", "Lock or unlock an NFT (locked NFTs cannot be transferred).", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| tokenId: z.number().int().describe("Token ID"), | ||
| locked: z.boolean().describe("true to lock, false to unlock"), | ||
| }, async ({ collectionId, tokenId, locked }) => tx(() => rc.nft.lock(w, { collectionId, tokenId, locked }))); | ||
| server.tool("nft_freeze_collection", "Freeze or unfreeze an entire collection (frozen collections cannot mint).", { | ||
| collectionId: z.string().describe("Collection ID"), | ||
| frozen: z.boolean().describe("true to freeze, false to unfreeze"), | ||
| }, async ({ collectionId, frozen }) => tx(() => rc.nft.freezeCollection(w, { collectionId, frozen }))); | ||
| // ── Name service ───────────────────────────────────────────────────────────── | ||
| server.tool("register_name", "Register a human-readable name (e.g. 'alice') to a wallet on the RougeChain name service.", { | ||
| name: z.string().describe("Name to register, without @domain"), | ||
| walletId: z.string().describe("Wallet ID / public key to bind the name to"), | ||
| }, async ({ name, walletId }) => tx(() => rc.mail.registerName(w, name, walletId))); | ||
| server.tool("release_name", "Release a name previously registered by the configured wallet.", { name: z.string().describe("Name to release") }, async ({ name }) => tx(() => rc.mail.releaseName(w, name))); | ||
| // ── On-chain social ────────────────────────────────────────────────────────── | ||
| server.tool("create_post", "Publish a post to the on-chain social timeline from the configured wallet.", { | ||
| body: z.string().describe("Post text"), | ||
| replyToId: z.string().optional().describe("Parent post ID to reply to (omit for a top-level post)"), | ||
| }, async ({ body, replyToId }) => tx(() => rc.social.createPost(w, body, replyToId))); | ||
| server.tool("delete_post", "Delete a post the configured wallet authored.", { postId: z.string().describe("Post ID") }, async ({ postId }) => tx(() => rc.social.deletePost(w, postId))); | ||
| server.tool("repost", "Toggle a repost of another user's post.", { postId: z.string().describe("Post ID to repost") }, async ({ postId }) => tx(() => rc.social.toggleRepost(w, postId))); | ||
| server.tool("follow", "Toggle following an artist/user by public key.", { pubkey: z.string().describe("Public key of the account to follow/unfollow") }, async ({ pubkey }) => tx(() => rc.social.toggleFollow(w, pubkey))); | ||
| server.tool("like_track", "Toggle a like on a music track / NFT.", { trackId: z.string().describe("Track or NFT token ID") }, async ({ trackId }) => tx(() => rc.social.toggleLike(w, trackId))); | ||
| server.tool("comment_on_track", "Post a comment on a music track / NFT.", { | ||
| trackId: z.string().describe("Track or NFT token ID"), | ||
| body: z.string().describe("Comment text"), | ||
| }, async ({ trackId, body }) => tx(() => rc.social.postComment(w, trackId, body))); | ||
| // ── Bridge ─────────────────────────────────────────────────────────────────── | ||
| server.tool("bridge_withdraw", "Withdraw a bridged asset from RougeChain to an EVM address (e.g. Base Sepolia).", { | ||
| amount: z.number().positive().describe("Amount to withdraw"), | ||
| evmAddress: z.string().describe("Destination EVM address (0x…)"), | ||
| tokenSymbol: z.string().optional().describe("Bridged token symbol (default qETH)"), | ||
| fee: z.number().optional().describe("Bridge fee (default 0.1)"), | ||
| }, async ({ amount, evmAddress, tokenSymbol, fee }) => tx(() => rc.bridge.withdraw(w, { amount, evmAddress, tokenSymbol, fee }))); | ||
| } | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // RESOURCES — static context about RougeChain for AI agents | ||
@@ -291,2 +526,8 @@ // ══════════════════════════════════════════════════════════════════════════════ | ||
| console.error(`[rougechain-mcp] API endpoint: ${API}`); | ||
| if (signer) { | ||
| console.error(`[rougechain-mcp] Signer loaded — write tools ENABLED (${signer.publicKey.slice(0, 16)}…)`); | ||
| } | ||
| else { | ||
| console.error("[rougechain-mcp] No signer — running READ-ONLY. Set ROUGECHAIN_MNEMONIC to enable transacting."); | ||
| } | ||
| } | ||
@@ -293,0 +534,0 @@ main().catch((err) => { |
+10
-3
| { | ||
| "name": "@rougechain/mcp-server", | ||
| "version": "1.0.0", | ||
| "description": "MCP server for RougeChain — AI agents interact with a post-quantum blockchain", | ||
| "version": "1.1.0", | ||
| "description": "MCP server for RougeChain — AI agents read AND transact on a post-quantum blockchain", | ||
| "type": "module", | ||
@@ -10,6 +10,12 @@ "main": "dist/index.js", | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "README.md" | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "start": "node dist/index.js", | ||
| "dev": "tsc && node dist/index.js" | ||
| "dev": "tsc && node dist/index.js", | ||
| "verify-pack": "node scripts/verify-pack.mjs", | ||
| "prepublishOnly": "node scripts/verify-pack.mjs" | ||
| }, | ||
@@ -19,2 +25,3 @@ "keywords": ["mcp", "rougechain", "blockchain", "post-quantum", "ai-agent"], | ||
| "@modelcontextprotocol/sdk": "^1.12.0", | ||
| "@rougechain/sdk": "^1.3.2", | ||
| "zod": "^3.23.0" | ||
@@ -21,0 +28,0 @@ }, |
+80
-9
| # RougeChain MCP Server | ||
| > AI agents can now interact with a post-quantum blockchain. | ||
| > AI agents can now **read and transact on** a post-quantum blockchain. | ||
| The **first MCP-native blockchain integration** — lets AI agents (Claude, ChatGPT, custom agents) read chain state, query tokens, check balances, deploy WASM smart contracts, and more using the [Model Context Protocol](https://modelcontextprotocol.io/). | ||
| The **first MCP-native blockchain integration** — lets AI agents (Claude, ChatGPT, custom agents) read chain state, query tokens, check balances, deploy WASM smart contracts, **and — with a wallet configured — sign and submit real transactions** (transfers, swaps, token/NFT minting, staking, social posts, and more) using the [Model Context Protocol](https://modelcontextprotocol.io/). | ||
| Every write is signed locally with **ML-DSA-65 (FIPS 204)** via [`@rougechain/sdk`](https://www.npmjs.com/package/@rougechain/sdk) — private keys never leave the server process. | ||
| ## Two modes | ||
| | Mode | How | What the agent can do | | ||
| |------|-----|-----------------------| | ||
| | **Read-only** (default) | no wallet env | All query tools. Safe to expose anywhere. | | ||
| | **Read + write** | set a wallet env (below) | Everything above **plus** signed transactions from that wallet. | | ||
| Write tools are **only registered when a wallet is configured** — with no wallet, the server is strictly read-only and the transaction tools don't even appear. | ||
| ## Quick Start | ||
| No install needed — the server is published on npm as [`@rougechain/mcp-server`](https://www.npmjs.com/package/@rougechain/mcp-server) and runs via `npx`. | ||
| ### Claude Desktop Config | ||
| Add to `~/.config/claude/claude_desktop_config.json`: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "rougechain": { | ||
| "command": "npx", | ||
| "args": ["-y", "@rougechain/mcp-server"], | ||
| "env": { | ||
| "ROUGECHAIN_URL": "https://api.rougechain.io" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| <details> | ||
| <summary>Run from source instead</summary> | ||
| ```bash | ||
@@ -15,6 +49,4 @@ cd mcp-server | ||
| ### Claude Desktop Config | ||
| Then point the config at the built file: | ||
| Add to `~/.config/claude/claude_desktop_config.json`: | ||
| ```json | ||
@@ -26,5 +58,3 @@ { | ||
| "args": ["/path/to/quantum-vault/mcp-server/dist/index.js"], | ||
| "env": { | ||
| "ROUGECHAIN_URL": "https://api.rougechain.io" | ||
| } | ||
| "env": { "ROUGECHAIN_URL": "https://api.rougechain.io" } | ||
| } | ||
@@ -34,2 +64,3 @@ } | ||
| ``` | ||
| </details> | ||
@@ -42,5 +73,45 @@ ### Environment Variables | ||
| | `ROUGECHAIN_API_KEY` | (none) | Optional API key | | ||
| | `ROUGECHAIN_MNEMONIC` | (none) | **Enables write mode.** 12/24-word BIP-39 seed of the signing wallet | | ||
| | `ROUGECHAIN_PRIVATE_KEY` + `ROUGECHAIN_PUBLIC_KEY` | (none) | Alternative to the mnemonic — raw hex keys | | ||
| ## Available Tools (29) | ||
| > ⚠️ **The mnemonic/private key controls real funds.** Only set it for a wallet you | ||
| > intend the agent to spend from, keep it out of shared configs, and prefer a | ||
| > low-balance "agent wallet". Need a fresh one? Call the `generate_wallet` tool. | ||
| ### Enabling write mode (Claude Desktop) | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "rougechain": { | ||
| "command": "npx", | ||
| "args": ["-y", "@rougechain/mcp-server"], | ||
| "env": { | ||
| "ROUGECHAIN_URL": "https://api.rougechain.io", | ||
| "ROUGECHAIN_MNEMONIC": "word1 word2 … word24" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ## Available Tools | ||
| ### Wallet (always available) | ||
| - `generate_wallet` — Create a fresh ML-DSA-65 wallet (mnemonic + address); not persisted | ||
| - `wallet_info` — Show the configured signer, its address, live balance, and whether writes are enabled | ||
| ### ✍️ Write / transaction tools (write mode only — signed with ML-DSA-65) | ||
| - **Value:** `send_transaction`, `burn_tokens`, `stake`, `unstake`, `request_faucet` (testnet) | ||
| - **Tokens:** `create_token`, `mint_tokens`, `update_token_metadata`, `claim_token_metadata` | ||
| - **DEX:** `swap`, `create_pool`, `add_liquidity`, `remove_liquidity` | ||
| - **NFTs:** `nft_create_collection`, `nft_mint`, `nft_batch_mint`, `nft_transfer`, `nft_burn`, `nft_lock`, `nft_freeze_collection` | ||
| - **Name service:** `register_name`, `release_name` | ||
| - **Social:** `create_post`, `delete_post`, `repost`, `follow`, `like_track`, `comment_on_track` | ||
| - **Bridge:** `bridge_withdraw` | ||
| --- | ||
| ### Read tools (always available) | ||
| ### Chain Info | ||
@@ -47,0 +118,0 @@ - `get_chain_stats` — Network stats (height, peers, validators, supply) |
-500
| #!/usr/bin/env node | ||
| /** | ||
| * RougeChain MCP Server | ||
| * | ||
| * Exposes RougeChain blockchain operations as MCP tools for AI agents. | ||
| * The first post-quantum, AI-agent-native programmable blockchain. | ||
| * | ||
| * Usage: | ||
| * ROUGECHAIN_URL=https://api.rougechain.io npx @rougechain/mcp-server | ||
| */ | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z } from "zod"; | ||
| // ─── Configuration ──────────────────────────────────────────────────────────── | ||
| // NOTE: this must be the API host (api.rougechain.io), NOT the frontend host | ||
| // (rougechain.io), which serves the SPA index.html for every /api/* path. | ||
| const BASE_URL = process.env.ROUGECHAIN_URL || "https://api.rougechain.io"; | ||
| const API = `${BASE_URL}/api`; | ||
| const API_KEY = process.env.ROUGECHAIN_API_KEY || ""; | ||
| // ─── HTTP helpers ───────────────────────────────────────────────────────────── | ||
| const headers: Record<string, string> = { | ||
| "Content-Type": "application/json", | ||
| ...(API_KEY ? { "X-API-Key": API_KEY } : {}), | ||
| }; | ||
| async function apiGet(path: string): Promise<unknown> { | ||
| const res = await fetch(`${API}${path}`, { headers }); | ||
| return res.json(); | ||
| } | ||
| async function apiPost(path: string, body: unknown): Promise<unknown> { | ||
| const res = await fetch(`${API}${path}`, { | ||
| method: "POST", | ||
| headers, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| return res.json(); | ||
| } | ||
| // ─── MCP Server ─────────────────────────────────────────────────────────────── | ||
| const server = new McpServer({ | ||
| name: "rougechain", | ||
| version: "1.0.0", | ||
| }); | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // TOOLS — actions that AI agents can perform on RougeChain | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // ── Chain Info ──────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "get_chain_stats", | ||
| "Get RougeChain network statistics: block height, peer count, validator count, total supply", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/stats"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_block", | ||
| "Get a block by height from the RougeChain blockchain", | ||
| { height: z.number().describe("Block height to retrieve") }, | ||
| async ({ height }) => { | ||
| const data = await apiGet(`/block/${height}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_latest_blocks", | ||
| "Get the most recent blocks from the chain", | ||
| { limit: z.number().optional().default(10).describe("Number of blocks to return (max 100)") }, | ||
| async ({ limit }) => { | ||
| const data = await apiGet(`/blocks?limit=${limit}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Wallet & Balance ───────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "get_balance", | ||
| "Check XRGE or token balance for a wallet address or public key", | ||
| { | ||
| address: z.string().describe("Wallet address (rouge1...) or public key hex"), | ||
| token: z.string().optional().describe("Token symbol (omit for XRGE native balance)"), | ||
| }, | ||
| async ({ address, token }) => { | ||
| const path = token | ||
| ? `/balance/${address}/${token}` | ||
| : `/balance/${address}`; | ||
| const data = await apiGet(path); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_transaction", | ||
| "Look up a specific transaction by hash", | ||
| { hash: z.string().describe("Transaction hash") }, | ||
| async ({ hash }) => { | ||
| const data = await apiGet(`/tx/${hash}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Token Operations ───────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_tokens", | ||
| "List all custom tokens on RougeChain with their metadata", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/tokens"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_token", | ||
| "Get detailed metadata for a specific token by symbol", | ||
| { symbol: z.string().describe("Token symbol (e.g. ROUGE)") }, | ||
| async ({ symbol }) => { | ||
| const data = await apiGet(`/token/${symbol}/metadata`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_token_holders", | ||
| "Get the top holders of a specific token", | ||
| { symbol: z.string().describe("Token symbol") }, | ||
| async ({ symbol }) => { | ||
| const data = await apiGet(`/token/${symbol}/holders`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── DeFi / AMM ─────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_pools", | ||
| "List all liquidity pools on RougeChain DEX", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/pools"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_swap_quote", | ||
| "Get a swap quote from the AMM (price, slippage, route)", | ||
| { | ||
| from: z.string().describe("Source token symbol"), | ||
| to: z.string().describe("Destination token symbol"), | ||
| amount: z.number().describe("Amount of source token to swap"), | ||
| }, | ||
| async ({ from, to, amount }) => { | ||
| const data = await apiPost("/swap/quote", { | ||
| token_in: from, | ||
| token_out: to, | ||
| amount_in: amount, | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── NFTs ────────────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_nft_collections", | ||
| "List all NFT collections on RougeChain", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/nft/collections"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_nft_collection", | ||
| "Get details and tokens for an NFT collection", | ||
| { symbol: z.string().describe("Collection symbol") }, | ||
| async ({ symbol }) => { | ||
| const data = await apiGet(`/nft/collection/${symbol}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Validators & Staking ───────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_validators", | ||
| "List all validators on the RougeChain network with their stake and status", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/validators"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── WASM Smart Contracts ───────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_contracts", | ||
| "List all deployed WASM smart contracts on RougeChain", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/contracts"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_contract", | ||
| "Get metadata for a deployed smart contract", | ||
| { address: z.string().describe("Contract address (hex)") }, | ||
| async ({ address }) => { | ||
| const data = await apiGet(`/contract/${address}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_contract_state", | ||
| "Read contract storage. Omit key to dump all state; provide key for single-value lookup.", | ||
| { | ||
| address: z.string().describe("Contract address"), | ||
| key: z.string().optional().describe("Storage key (hex or string). Omit to dump all state."), | ||
| }, | ||
| async ({ address, key }) => { | ||
| const path = key | ||
| ? `/contract/${address}/state?key=${encodeURIComponent(key)}` | ||
| : `/contract/${address}/state`; | ||
| const data = await apiGet(path); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_contract_events", | ||
| "Get the event log for a smart contract", | ||
| { | ||
| address: z.string().describe("Contract address"), | ||
| limit: z.number().optional().default(50).describe("Max events to return"), | ||
| }, | ||
| async ({ address, limit }) => { | ||
| const data = await apiGet(`/contract/${address}/events?limit=${limit}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "deploy_contract", | ||
| "Deploy a WASM smart contract to RougeChain. Requires base64-encoded WASM bytecode.", | ||
| { | ||
| wasm: z.string().describe("Base64-encoded WASM bytecode"), | ||
| deployer: z.string().describe("Deployer's public key hex"), | ||
| nonce: z.number().optional().default(0).describe("Nonce for deterministic address"), | ||
| }, | ||
| async ({ wasm, deployer, nonce }) => { | ||
| const data = await apiPost("/v2/contract/deploy", { wasm, deployer, nonce }); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "call_contract", | ||
| "Call a method on a deployed WASM smart contract", | ||
| { | ||
| contractAddr: z.string().describe("Contract address (hex)"), | ||
| method: z.string().describe("Method name to call"), | ||
| caller: z.string().optional().describe("Caller's public key"), | ||
| args: z.record(z.unknown()).optional().describe("JSON arguments for the method"), | ||
| gasLimit: z.number().optional().describe("Gas limit (default 10M)"), | ||
| }, | ||
| async ({ contractAddr, method, caller, args, gasLimit }) => { | ||
| const data = await apiPost("/v2/contract/call", { | ||
| contractAddr, | ||
| method, | ||
| caller, | ||
| args: args || {}, | ||
| gasLimit, | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Governance ─────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "list_proposals", | ||
| "List governance proposals on RougeChain", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/governance/proposals"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Fee Info ────────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "get_fee_info", | ||
| "Get current EIP-1559 dynamic fee information (base fee, priority fee, burned fees)", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/fee-info"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Name Service ───────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "resolve_name", | ||
| "Resolve a mail name (e.g. 'alice') to the wallet's public keys and encryption key. Names are registered as alice@rouge.quant or alice@qwalla.mail", | ||
| { name: z.string().describe("Name to resolve (e.g. 'alice', without the @domain)") }, | ||
| async ({ name }) => { | ||
| const data = await apiGet(`/names/resolve/${encodeURIComponent(name.toLowerCase())}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "reverse_lookup_name", | ||
| "Look up the registered mail name for a wallet ID or public key", | ||
| { walletId: z.string().describe("Wallet ID or public key hex") }, | ||
| async ({ walletId }) => { | ||
| const data = await apiGet(`/names/reverse/${encodeURIComponent(walletId)}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_messenger_wallets", | ||
| "List all registered messenger wallets with their display names and encryption keys", | ||
| {}, | ||
| async () => { | ||
| const data = await apiGet("/messenger/wallets"); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ── Social ─────────────────────────────────────────────────────────────────── | ||
| server.tool( | ||
| "get_global_timeline", | ||
| "Get the global social timeline — all posts, newest first", | ||
| { | ||
| limit: z.number().optional().default(50).describe("Max posts to return"), | ||
| offset: z.number().optional().default(0).describe("Offset for pagination"), | ||
| }, | ||
| async ({ limit, offset }) => { | ||
| const data = await apiGet(`/social/timeline?limit=${limit}&offset=${offset}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_post", | ||
| "Get a single social post by ID with engagement stats", | ||
| { | ||
| postId: z.string().describe("Post ID (UUID)"), | ||
| viewer: z.string().optional().describe("Viewer public key to check liked/reposted state"), | ||
| }, | ||
| async ({ postId, viewer }) => { | ||
| const q = viewer ? `?viewer=${encodeURIComponent(viewer)}` : ""; | ||
| const data = await apiGet(`/social/post/${encodeURIComponent(postId)}${q}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_user_posts", | ||
| "Get posts by a specific user", | ||
| { | ||
| pubkey: z.string().describe("User's public key"), | ||
| limit: z.number().optional().default(50).describe("Max posts to return"), | ||
| }, | ||
| async ({ pubkey, limit }) => { | ||
| const data = await apiGet(`/social/user/${encodeURIComponent(pubkey)}/posts?limit=${limit}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_post_replies", | ||
| "Get threaded replies to a post", | ||
| { | ||
| postId: z.string().describe("Parent post ID"), | ||
| limit: z.number().optional().default(50).describe("Max replies to return"), | ||
| }, | ||
| async ({ postId, limit }) => { | ||
| const data = await apiGet(`/social/post/${encodeURIComponent(postId)}/replies?limit=${limit}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_track_stats", | ||
| "Get social stats for a music track (plays, likes, comments)", | ||
| { | ||
| trackId: z.string().describe("Track/NFT token ID"), | ||
| viewer: z.string().optional().describe("Viewer public key to check liked state"), | ||
| }, | ||
| async ({ trackId, viewer }) => { | ||
| const q = viewer ? `?viewer=${encodeURIComponent(viewer)}` : ""; | ||
| const data = await apiGet(`/social/track/${encodeURIComponent(trackId)}/stats${q}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_artist_stats", | ||
| "Get social stats for an artist (followers, following count)", | ||
| { | ||
| pubkey: z.string().describe("Artist's public key"), | ||
| viewer: z.string().optional().describe("Viewer public key to check follow state"), | ||
| }, | ||
| async ({ pubkey, viewer }) => { | ||
| const q = viewer ? `?viewer=${encodeURIComponent(viewer)}` : ""; | ||
| const data = await apiGet(`/social/artist/${encodeURIComponent(pubkey)}/stats${q}`); | ||
| return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; | ||
| } | ||
| ); | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // RESOURCES — static context about RougeChain for AI agents | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| server.resource( | ||
| "chain-info", | ||
| "rougechain://info", | ||
| async (uri) => ({ | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "text/plain", | ||
| text: `RougeChain — The First Post-Quantum Programmable Blockchain | ||
| Core Technology: | ||
| - ML-DSA-65 (FIPS 204) digital signatures | ||
| - ML-KEM-768 (FIPS 203) key encapsulation | ||
| - Bech32m addresses (rouge1...) | ||
| - ZK-STARK proofs (winterfell) for shielded transactions | ||
| - WASM smart contracts (wasmi runtime) | ||
| - EIP-1559 dynamic fees with fee burning | ||
| Native Token: XRGE | ||
| Address Format: rouge1... (Bech32m) | ||
| Consensus: Proof of Stake with BFT finality | ||
| API Base: ${API} | ||
| Features: | ||
| - Custom tokens with mint authority | ||
| - NFT collections with royalties | ||
| - AMM DEX with multi-hop routing | ||
| - End-to-end encrypted messaging (ML-KEM-768 + AES-GCM) | ||
| - Encrypted mail with @rouge.quant / @qwalla.mail addresses (CEK multi-recipient encryption) | ||
| - Social layer: posts, timeline, threaded replies, reposts, likes, follows, comments, tips | ||
| - Real-time notifications: unread badges, native browser notifications, push notifications | ||
| - EVM bridge (Base Sepolia) | ||
| - Name service (mail + wallet name registry) | ||
| - Governance proposals | ||
| - WASM smart contracts with fuel-metered execution | ||
| - WebSocket real-time event streaming | ||
| SDK: @rougechain/sdk (npm) | ||
| Docs: ${BASE_URL}/docs`, | ||
| }, | ||
| ], | ||
| }) | ||
| ); | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| // Start the server | ||
| // ══════════════════════════════════════════════════════════════════════════════ | ||
| async function main() { | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| console.error("[rougechain-mcp] Server started — connected via stdio"); | ||
| console.error(`[rougechain-mcp] API endpoint: ${API}`); | ||
| } | ||
| main().catch((err) => { | ||
| console.error("[rougechain-mcp] Fatal error:", err); | ||
| process.exit(1); | ||
| }); |
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "Node16", | ||
| "moduleResolution": "Node16", | ||
| "outDir": "./dist", | ||
| "rootDir": "./src", | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "declaration": true, | ||
| "resolveJsonModule": true | ||
| }, | ||
| "include": ["src/**/*"] | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances
44415
4.81%180
65.14%3
-25%3
50%4
-33.33%547
-27.26%7
16.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added