@qianexchange/mcp: MCP server for the Qian multichain DEX and launchpad

This commit is contained in:
Qian
2026-07-17 07:39:43 +00:00
commit ee0d204ef4
7 changed files with 1649 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
npm-debug.log*
.env
*.tgz
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Qian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
# @qianexchange/mcp
MCP server for [Qian](https://qian.ag), the multichain DEX and launchpad. Any MCP-capable agent can launch tokens, trade, and read market data on Robinhood Chain, Base, and Ethereum.
Non-custodial: the server runs on your machine and signs with your key. Qian never holds keys or funds. Every launched token's liquidity is locked forever in QianLocker.
## Setup
```json
{
"mcpServers": {
"qian": {
"command": "npx",
"args": ["-y", "@qianexchange/mcp"],
"env": { "QIAN_KEY": "0x<your-agent-wallet-private-key>" }
}
}
}
```
Omit `QIAN_KEY` for read-only mode (market data still works). Use a dedicated hot wallet funded with only what the agent may spend.
## Tools
| Tool | What it does |
|---|---|
| `list_tokens` | Tokens on Qian, all chains, with price/FDV/volume |
| `get_token` | Full detail: price, FDV, hook, metadata |
| `get_trades` / `get_holders` | Market activity |
| `my_wallet` | Your address + ETH balance per chain |
| `quote_buy` | Simulate a buy (no gas) |
| `buy` / `sell` | Trade via QianSwapRouter (plain ETH in/out, auto-approve, slippage-protected) |
| `launch_token` | Deploy a token: name/symbol/image/socials, FDV tier 1–4, creator fee 0.5–20%, optional dev buy |
| `collect_fees` | Claim your 70% creator share of trading fees |
## Notes
- V2 has no launch-time trading restrictions. Liquidity is locked forever; there is no dead window or max-wallet cap.
- Trades route through QianSwapRouter, which takes a 1% fee on the ETH side.
- Env overrides: `QIAN_API`, `RPC_ROBINHOOD`, `RPC_BASE`, `RPC_ETHEREUM`.
MIT. Contact: dev@qian.ag
+12
View File
@@ -0,0 +1,12 @@
# Security
The Qian MCP server is non-custodial. It runs on your own machine and signs transactions locally with the private key you place in `QIAN_KEY`. That key never leaves your environment, and Qian's servers never receive it.
Guidance:
- Use a dedicated hot wallet for the agent. Fund it with only what the agent is allowed to spend.
- Never commit `QIAN_KEY` or an `.env` holding it. The included `.gitignore` excludes `.env`.
- Read tools (market data) work without `QIAN_KEY`. Only set the key when the agent needs to launch, trade, or collect fees.
- Every launched token's liquidity is locked forever in QianLocker; the contracts have no owner, mint, pause, or blacklist functions.
To report a vulnerability, email dev@qian.ag.
+1288
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@qianexchange/mcp",
"version": "0.1.0",
"description": "MCP server for the Qian multichain DEX & launchpad (qian.ag) — launch and trade tokens from any MCP-capable agent",
"type": "module",
"bin": { "qian-mcp": "server.js" },
"author": "[email protected]",
"license": "MIT",
"engines": { "node": ">=20" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.0",
"ethers": "^6.13.0",
"zod": "^3.24.0"
}
}
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env node
/**
* Qian MCP server — launch & trade tokens on the Qian multichain DEX (qian.ag).
*
* Read tools proxy the public API (api.qian.ag). Write tools sign with the key
* in QIAN_KEY (the agent's own hot wallet — Qian never holds keys or funds).
* Chains: robinhood (4663), base (8453), ethereum (1).
*
* Env: QIAN_KEY (hex private key, required for write tools),
* QIAN_API (default https://api.qian.ag),
* RPC_ROBINHOOD / RPC_BASE / RPC_ETHEREUM (optional RPC overrides).
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { ethers } from 'ethers'
const API = process.env.QIAN_API ?? 'https://api.qian.ag'
// V2 stack (2026-07-16): core addresses are IDENTICAL on every chain (one fresh
// key at nonce 0 each). No feature hooks — the only approved hook is the GuardHook,
// applied automatically when deployCoin passes hookAddress=0, and the creator's tax
// is the V4 pool fee. Only rpc/chainId/explorer differ per chain.
const CORE = {
factoryV4: '0x750d0bc66baa12CF1710a8ba939B97DB4aD005c3',
feeRouter: '0xDe598E5F0b428B38EFaA6e887458FEF70bFA9436',
swapRouter: '0xEE540d2496cfFDf17E29a04bcd7b6dc795C61d66',
}
const CHAINS = {
robinhood: {
...CORE,
chainId: 4663,
rpc: process.env.RPC_ROBINHOOD ?? 'https://rpc.mainnet.chain.robinhood.com',
explorer: 'https://robinhoodchain.blockscout.com',
},
base: {
...CORE,
chainId: 8453,
rpc: process.env.RPC_BASE ?? 'https://base-rpc.publicnode.com',
explorer: 'https://basescan.org',
},
ethereum: {
...CORE,
chainId: 1,
rpc: process.env.RPC_ETHEREUM ?? 'https://ethereum-rpc.publicnode.com',
explorer: 'https://etherscan.io',
},
}
const chainSlug = z.enum(['robinhood', 'base', 'ethereum'])
const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/, 'EVM address')
const SUPPLY = 1_000_000_000n * 10n ** 18n
const FACTORY_ABI = [
'function deployCoin(string _name, string _symbol, string _metadata, bytes32 salt, uint256 configId, address hookAddress, bytes hookConfig, uint256 maxDeployFee, uint24 poolFee) payable',
'function deployFee() view returns (uint256)',
'event ERC20TokenCreated(address tokenAddress)',
]
const ROUTER_ABI = [
'function buy(address token, uint256 minTokensOut) payable returns (uint256)',
'function sell(address token, uint256 tokensIn, uint256 minEthOut) returns (uint256)',
]
const FEE_ROUTER_ABI = ['function collectFees(address token)']
const ERC20_ABI = [
'function balanceOf(address) view returns (uint256)',
'function allowance(address,address) view returns (uint256)',
'function approve(address,uint256) returns (bool)',
]
const providers = {}
const provider = (c) => (providers[c] ??= new ethers.JsonRpcProvider(CHAINS[c].rpc, CHAINS[c].chainId, { staticNetwork: true }))
function wallet(c) {
if (!process.env.QIAN_KEY) throw new Error('QIAN_KEY not set — write tools need a private key in the environment')
return new ethers.Wallet(process.env.QIAN_KEY, provider(c))
}
async function api(path, init) {
const res = await fetch(`${API}${path}`, init)
if (!res.ok) throw new Error(`Qian API ${path} -> ${res.status}: ${(await res.text()).slice(0, 300)}`)
return res.json()
}
const json = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] })
// Trim a raw API token to what an agent needs; price derived from X96 sqrt-price.
function slim(t) {
const priceEth = t.last_price_x96 ? priceEthPerToken(t.last_price_x96, t.address, t.chain) : null
return {
chain: t.chain, address: t.address, name: t.name, symbol: t.symbol,
pool: t.pool_kind, creator: t.creator, deploy_time: t.deploy_time,
trade_count: t.trade_count, volume_eth_24h: t.volume_eth ? ethers.formatEther(BigInt(t.volume_eth)) : '0',
price_eth: priceEth, fdv_eth: priceEth === null ? null : priceEth * 1e9,
description: t.description, website: t.website, telegram: t.telegram, x: t.x,
url: `https://qian.ag/${t.chain}/token/${t.address}`,
}
}
const WETH = { robinhood: '0x0bd7d308f8e1639fab988df18a8011f41eacad73', base: '0x4200000000000000000000000000000000000006', ethereum: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' }
function priceEthPerToken(x96, token, chain) {
// sqrtPriceX96 is token1-per-token0; flip when the token sorts after WETH
const p = Number(BigInt(x96)) / 2 ** 96
const tokenIs0 = token.toLowerCase() < WETH[chain]
return tokenIs0 ? p * p : 1 / (p * p)
}
const server = new McpServer({ name: 'qian', version: '0.1.0' })
server.registerTool('list_tokens', {
description: 'List tokens launched on Qian (all chains, newest first). Optionally filter by chain.',
inputSchema: { chain: chainSlug.optional(), limit: z.number().int().min(1).max(200).default(50) },
}, async ({ chain, limit }) => {
let { tokens } = await api('/tokens')
if (chain) tokens = tokens.filter((t) => t.chain === chain)
return json(tokens.slice(0, limit).map(slim))
})
server.registerTool('get_token', {
description: 'Full detail for one Qian token: price, FDV, hook, metadata.',
inputSchema: { chain: chainSlug, address },
}, async ({ chain, address: addr }) => {
const { token } = await api(`/tokens/${chain}/${addr}`)
return json({
...slim(token),
hook: token.hook, hook_config: token.hook_config,
})
})
server.registerTool('get_trades', {
description: 'Recent trades for a Qian token (newest first).',
inputSchema: { chain: chainSlug, address, limit: z.number().int().min(1).max(200).default(50) },
}, async ({ chain, address: addr, limit }) => {
const { trades } = await api(`/tokens/${chain}/${addr}/trades?limit=${limit}`)
return json(trades)
})
server.registerTool('get_holders', {
description: 'Top holders for a Qian token.',
inputSchema: { chain: chainSlug, address },
}, async ({ chain, address: addr }) => json((await api(`/tokens/${chain}/${addr}/holders`)).holders))
server.registerTool('my_wallet', {
description: 'Your wallet address (from QIAN_KEY) and native ETH balance on each chain.',
inputSchema: {},
}, async () => {
const w = wallet('robinhood')
const balances = {}
for (const c of Object.keys(CHAINS)) balances[c] = ethers.formatEther(await provider(c).getBalance(w.address))
return json({ address: w.address, eth: balances })
})
server.registerTool('quote_buy', {
description: 'Simulate buying a token with ETH via QianSwapRouter (1% router fee included). Returns expected tokens out. Your wallet must hold at least eth_amount for the simulation.',
inputSchema: { chain: chainSlug, address, eth_amount: z.string().describe('ETH to spend, e.g. "0.05"') },
}, async ({ chain, address: addr, eth_amount }) => {
const router = new ethers.Contract(CHAINS[chain].swapRouter, ROUTER_ABI, wallet(chain))
const out = await router.buy.staticCall(addr, 0n, { value: ethers.parseEther(eth_amount) })
return json({ eth_in: eth_amount, tokens_out: ethers.formatEther(out), pct_of_supply: Number((out * 10000n) / SUPPLY) / 100 })
})
async function sendAndWait(txPromise, chain, label) {
const tx = await txPromise
const rc = await tx.wait()
if (rc.status !== 1) throw new Error(`${label} tx reverted: ${tx.hash}`)
return { hash: tx.hash, explorer: `${CHAINS[chain].explorer}/tx/${tx.hash}`, rc }
}
server.registerTool('buy', {
description: 'Buy a Qian token with plain ETH via QianSwapRouter. Simulates first, applies slippage to set min-out, then sends. No launch-time trading restrictions in V2 (no dead window, no max-wallet).',
inputSchema: {
chain: chainSlug, address,
eth_amount: z.string().describe('ETH to spend, e.g. "0.05"'),
slippage_bps: z.number().int().min(1).max(2000).default(100),
},
}, async ({ chain, address: addr, eth_amount, slippage_bps }) => {
const w = wallet(chain)
const router = new ethers.Contract(CHAINS[chain].swapRouter, ROUTER_ABI, w)
const value = ethers.parseEther(eth_amount)
const expected = await router.buy.staticCall(addr, 0n, { value })
const minOut = (expected * BigInt(10_000 - slippage_bps)) / 10_000n
const { hash, explorer } = await sendAndWait(router.buy(addr, minOut, { value }), chain, 'buy')
const bal = await new ethers.Contract(addr, ERC20_ABI, provider(chain)).balanceOf(w.address)
return json({ hash, explorer, spent_eth: eth_amount, tokens_received: ethers.formatEther(expected), wallet_balance: ethers.formatEther(bal) })
})
server.registerTool('sell', {
description: 'Sell a Qian token for ETH via QianSwapRouter (approves the router if needed). amount "all" sells your full balance.',
inputSchema: {
chain: chainSlug, address,
amount: z.string().describe('Token amount in whole tokens, or "all"'),
slippage_bps: z.number().int().min(1).max(2000).default(100),
},
}, async ({ chain, address: addr, amount, slippage_bps }) => {
const w = wallet(chain)
const erc = new ethers.Contract(addr, ERC20_ABI, w)
const router = new ethers.Contract(CHAINS[chain].swapRouter, ROUTER_ABI, w)
const tokens = amount === 'all' ? await erc.balanceOf(w.address) : ethers.parseEther(amount)
if (tokens === 0n) throw new Error('nothing to sell — token balance is 0')
if ((await erc.allowance(w.address, CHAINS[chain].swapRouter)) < tokens)
await sendAndWait(erc.approve(CHAINS[chain].swapRouter, ethers.MaxUint256), chain, 'approve')
const expected = await router.sell.staticCall(addr, tokens, 0n)
const minOut = (expected * BigInt(10_000 - slippage_bps)) / 10_000n
const { hash, explorer } = await sendAndWait(router.sell(addr, tokens, minOut), chain, 'sell')
return json({ hash, explorer, tokens_sold: ethers.formatEther(tokens), eth_received: ethers.formatEther(expected) })
})
server.registerTool('launch_token', {
description: [
'Launch a new token on Qian: 1B supply, full supply as locked one-sided liquidity (LP owned forever by QianLocker — no rug possible), fees stream to you as creator (70%) and the platform (30%).',
'Costs the deploy fee (~0.0005 ETH) + optional dev_buy_eth. Tiers set starting FDV: 1=1 ETH, 2=2, 3=5, 4=10.',
'creator_fee_pct is the trading tax (0.5%–20%) you earn on every swap — it is the V4 pool fee, immutable once launched. There are no anti-snipe / dead-window / max-wallet options in V2.',
].join(' '),
inputSchema: {
chain: chainSlug,
name: z.string().min(1).max(64),
symbol: z.string().min(1).max(16),
description: z.string().max(280).default(''),
image_url: z.string().url().optional().describe('http(s) or ipfs image; pinned to IPFS for you'),
website: z.string().default(''), telegram: z.string().default(''), x: z.string().default(''),
tier: z.number().int().min(1).max(4).default(1),
creator_fee_pct: z.number().min(0.5).max(20).default(1).describe('trading fee you earn, 0.5–20% (the immutable V4 pool fee)'),
dev_buy_eth: z.string().default('0').describe('ETH for an initial creator buy in the deploy tx'),
},
}, async (p) => {
const w = wallet(p.chain)
const C = CHAINS[p.chain]
let imageCid = ''
if (p.image_url) {
const src = p.image_url.startsWith('ipfs://') ? `https://ipfs.qian.ag/ipfs/${p.image_url.slice(7)}` : p.image_url
const img = await fetch(src)
if (!img.ok) throw new Error(`image fetch failed: ${img.status}`)
const body = new FormData()
body.append('file', new Blob([await img.arrayBuffer()], { type: img.headers.get('content-type') ?? 'image/png' }), 'logo')
imageCid = (await api('/launch/upload-image', { method: 'POST', body })).cid
}
const { uri } = await api('/launch/prepare', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: p.name, symbol: p.symbol, description: p.description, website: p.website, telegram: p.telegram, x: p.x, imageCid }),
})
// hookAddress=0 resolves to the GuardHook (front-run protection only); no feature hooks in V2.
const hookAddr = ethers.ZeroAddress, hookConfig = '0x'
const poolFee = Math.round(p.creator_fee_pct * 10_000) // uint24 hundredths of a bip (0.5%–20% → 5000–200000)
const factory = new ethers.Contract(C.factoryV4, FACTORY_ABI, w)
const fee = await factory.deployFee()
const value = fee + ethers.parseEther(p.dev_buy_eth)
const salt = ethers.hexlify(ethers.randomBytes(32))
// maxDeployFee = fee just read → the controller can't raise deployFee under our pending launch.
await factory.deployCoin.staticCall(p.name, p.symbol, uri, salt, p.tier, hookAddr, hookConfig, fee, poolFee, { value })
const { hash, explorer, rc } = await sendAndWait(
factory.deployCoin(p.name, p.symbol, uri, salt, p.tier, hookAddr, hookConfig, fee, poolFee, { value }), p.chain, 'deploy')
const created = rc.logs.map((l) => { try { return factory.interface.parseLog(l) } catch { return null } }).find((l) => l?.name === 'ERC20TokenCreated')
if (!created) throw new Error(`deploy succeeded but no ERC20TokenCreated event found in ${hash}`)
const token = created.args[0]
return json({ token, hash, explorer, page: `https://qian.ag/${p.chain}/token/${token}`, metadata_uri: uri, deploy_fee_eth: ethers.formatEther(fee) })
})
server.registerTool('collect_fees', {
description: 'Collect accrued trading fees for a token via QianFeeRouter (permissionless — anyone can trigger; payout always goes 70% to the token creator, 30% to the platform).',
inputSchema: { chain: chainSlug, address },
}, async ({ chain, address: addr }) => {
const feeRouter = new ethers.Contract(CHAINS[chain].feeRouter, FEE_ROUTER_ABI, wallet(chain))
const { hash, explorer } = await sendAndWait(feeRouter.collectFees(addr), chain, 'collectFees')
return json({ hash, explorer })
})
await server.connect(new StdioServerTransport())
console.error(`qian-mcp ready — api=${API} write=${process.env.QIAN_KEY ? 'enabled' : 'DISABLED (no QIAN_KEY)'}`)