index.js•11.2 kB
const dotenv = require("dotenv");
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { z } = require("zod");
const { Connection, Keypair, PublicKey } = require("@solana/web3.js");
const { getMint, NATIVE_MINT } = require("@solana/spl-token")
const { Raydium, TxVersion, getPdaLaunchpadConfigId, LAUNCHPAD_PROGRAM } = require("@raydium-io/raydium-sdk-v2");
const BN = require("bn.js")
const { PinataSDK } = require("pinata");
const fs = require("fs");
// Load environment variables from .env file
dotenv.config();
// Validate environment variables
const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!RPC_URL || !PRIVATE_KEY) {
throw new Error("Missing RPC_URL or PRIVATE_KEY in environment variables");
}
// Initialize keypair from private key (integer array)
const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(PRIVATE_KEY)));
const cluster = "mainnet"
const programId = LAUNCHPAD_PROGRAM
const platformId = new PublicKey("GnLd8ohnZ588sGntFFhSK9M9FyU7n3YBzsvcMwqQtoCf")
// Initialize Solana connection
const connection = new Connection(RPC_URL, "confirmed");
let raydium
async function initSdk(){
if(raydium) return raydium
raydium = await Raydium.load({
owner: keypair, connection, cluster,
disableFeatureCheck: true,
disableLoadToken: true,
blockhashCommitment: "finalized"
})
return raydium
}
function floatToBN(amount, decimals) {
const [whole, fraction = ''] = amount.toString().split('.');
const fractionPadded = (fraction + '0'.repeat(decimals)).slice(0, decimals);
return new BN(whole + fractionPadded);
}
async function uploadImageAndMetadata(imagePath, name, symbol, description="") {
if(!process.env.PINATA_GATEWAY || !process.env.PINATA_JWT){
throw new Error("Missing PINATA_GATEWAY or PINATA_JWT in environment variables");
}
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
pinataGateway: process.env.PINATA_GATEWAY
});
// Step 1: Upload image
const imageFile = new File(
[fs.readFileSync(imagePath)],
"logo.png",
{ type: "image/png" }
);
const imageUpload = await pinata.upload.public.file(imageFile);
// Construct image URL using the CID
const imageUrl = `https://gateway.pinata.cloud/ipfs/${imageUpload.cid}`;
// Step 2: Create and upload metadata
const metadata = {
name,
symbol,
description,
image: imageUrl
};
const metadataFile = new File(
[JSON.stringify(metadata)],
"metadata.json",
{ type: "application/json" }
);
const metadataUpload = await pinata.upload.public.file(metadataFile);
// Construct metadata URL
const metadataUrl = `https://gateway.pinata.cloud/ipfs/${metadataUpload.cid}`;
return {
imageUrl,
metadataUrl,
metadata
};
}
// Create MCP server
const server = new McpServer({
name: "Raydium LaunchLab MCP",
version: "1.0.0",
});
// Register buy_token tool
server.tool(
"buy_token",
"Purchase tokens from a Raydium Launchpad pool using the token mint address",
z.object({
mintAddress: z
.string()
.refine((val) => PublicKey.isOnCurve(val), {
message: "Invalid token mint address",
})
.describe("The mint address of the token to purchase"),
inAmount: z
.number()
.positive("Amount must be positive")
.describe("The amount of SOL used to buy tokens"),
slippage: z
.number()
.positive("Slippage must be positive")
.max(1.0, "Slippage must not exceed 100%")
.optional()
.default(0.01)
.describe("The acceptable price slippage percentage (e.g., 0.01 for 1%)"),
}),
async ({ mintAddress, inAmount, slippage=0.01 }) => {
try {
const raydium = await initSdk();
const mintA = new PublicKey(mintAddress);
const buyAmount = floatToBN(inAmount, 9);
const slippageBN = floatToBN(slippage, 4);
const { transaction, extInfo, execute } = await raydium.launchpad.buyToken({
programId,
mintA,
buyAmount,
slippage: slippageBN,
txVersion: TxVersion.V0,
});
// Sign and send transaction
const sentInfo = await execute({ sendAndConfirm: true });
return {
content: [
{
type: "text",
text: `Successfully purchased tokens (mint: ${mintAddress}). Transaction signature: ${sentInfo.txId}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error purchasing token: ${error.message}`,
},
],
isError: true,
};
}
}
);
// Register sell_token tool
server.tool(
"sell_token",
"Sell tokens from a Raydium Launchpad pool using the token mint address",
z.object({
mintAddress: z
.string()
.refine((val) => PublicKey.isOnCurve(val), {
message: "Invalid token mint address",
})
.describe("The mint address of the token to sell"),
inAmount: z
.number()
.positive("Amount must be positive")
.describe("The amount of tokens to sell"),
slippage: z
.number()
.positive("Slippage must be positive")
.max(1.0, "Slippage must not exceed 100%")
.optional()
.default(0.01)
.describe("The acceptable price slippage percentage (e.g., 0.01 for 1%)"),
}),
async ({ mintAddress, inAmount, slippage=0.01 }) => {
try {
const raydium = await initSdk();
// Fetch token decimals
const mintA = new PublicKey(mintAddress);
const mintInfo = await getMint(connection, mintA);
const decimals = mintInfo.decimals;
const sellAmount = floatToBN(inAmount, decimals);
const slippageBN = floatToBN(slippage, 4)
const { transaction, extInfo, execute } = await raydium.launchpad.sellToken({
programId,
mintA,
sellAmount,
slippage: slippageBN,
txVersion: TxVersion.V0,
});
// Sign and send transaction
const sentInfo = await execute({ sendAndConfirm: true });
return {
content: [
{
type: "text",
text: `Successfully sold tokens (mint: ${mintAddress}). Transaction signature: ${sentInfo.txId}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error selling token: ${error.message}`,
},
],
isError: true,
};
}
}
);
// Register mint_token tool
server.tool(
"mint_token",
"Create a bonding-curve based token on Raydium Launchpad",
z.object({
name: z
.string()
.min(1, "Token name must not be empty")
.max(32, "Token name must not exceed 32 characters")
.describe("The name of the token (e.g., 'My Token')"),
symbol: z
.string()
.min(1, "Token symbol must not be empty")
.max(10, "Token symbol must not exceed 10 characters")
.describe("The symbol of the token (e.g., 'MTK')"),
imagePath: z
.string()
.describe("Token image file path (e.g., '/assets/logo.png')"),
decimals: z
.number()
.int("Decimals must be an integer")
.min(0, "Decimals must be non-negative")
.max(18, "Decimals must not exceed 18")
.default(6)
.describe("The number of decimal places for the token (e.g., 6 for USDC-like precision)"),
fundRaisingTarget: z
.number()
.positive("Fundraising target must be positive")
.default(85)
.describe("The target SOL amount to raise for the bonding-curve"),
totalSupply: z
.number()
.default(1_000_000_000)
.describe("The total supply of the token"),
totalSellPercent: z
.number()
.default(0.75)
.describe("The percentage of total supply for fund raising"),
createOnly: z
.boolean()
.default(true)
.describe("Whether to only create the token without buying (true) or include buying (false)"),
initialBuyAmount: z
.number()
.nonnegative("Buy amount must be non-negative")
.default(0.1)
.describe("The amount of SOL to use for initial token purchase, only available if createOnly is false"),
slippage: z
.number()
.positive("Slippage must be positive")
.max(1.0, "Slippage must not exceed 100%")
.optional()
.default(0.01)
.describe("The acceptable price slippage percentage for initial purchase (e.g., 0.01 for 1%)"),
}),
async ({
name, symbol, imagePath,
decimals=6, totalSupply=1000000000, totalSellPercent=0.75,
fundRaisingTarget=85, createOnly=true, initialBuyAmount=0.1, slippage=0.01
}) => {
try {
//upload image file and metadata
const {imageUrl, metadataUrl, metadata} = await uploadImageAndMetadata(imagePath, name, symbol)
const raydium = await initSdk();
// Create new mint keypair
const mintKeypair = Keypair.generate();
const mintA = mintKeypair.publicKey;
const configId = getPdaLaunchpadConfigId(programId, NATIVE_MINT, 0, 0).publicKey;
// Convert float to BN
const totalSupplyBN = floatToBN(totalSupply, decimals);
const totalSellAmountBN = floatToBN(totalSupply * totalSellPercent, decimals);
const fundRaisingTargetBN = floatToBN(fundRaisingTarget, 9); //SOL
const initialBuyAmountBN = floatToBN(initialBuyAmount, 9); //SOL
const slippageBN = floatToBN(slippage, 4);
const { execute, transactions, extInfo } = await raydium.launchpad.createLaunchpad({
programId,
mintA,
decimals,
name,
symbol,
migrateType: 'cpmm',
uri: metadataUrl,
configId,
platformId,
txVersion: TxVersion.V0,
extraSigners: [mintKeypair],
totalFundRaisingB: fundRaisingTargetBN, //SOL
supply: totalSupplyBN,
totalSellA: totalSellAmountBN,
createOnly,
buyAmount: initialBuyAmountBN, //SOL
slippage: slippageBN,
})
// Sign and send transaction
const sentInfo = await execute({ sendAndConfirm: true });
return {
content: [
{
type: "text",
text: `Successfully created token (mint: ${mintA.toBase58()}, name: ${name}, symbol: ${symbol}). Transaction signatures: ${sentInfo.txIds.join(",")}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating token mint: ${error.message}`,
},
],
isError: true,
};
}
}
);
// Main function to start the server
async function main() {
const transport = new StdioServerTransport();
// Connect MCP server to transport
await server.connect(transport);
}
// Start the server
main().catch(error => console.error);