# Directory Structure
```
├── .github
│ └── demo.png
├── .gitignore
├── .prettierrc.js
├── LICENSE
├── package.json
├── README.md
├── src
│ ├── kaiafun-mcp-server.ts
│ └── sdk
│ ├── abi.ts
│ ├── chain.ts
│ ├── client.ts
│ ├── constants.ts
│ ├── index.ts
│ └── utils.ts
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
```javascript
module.exports = {
semi: true,
singleQuote: true,
printWidth: 100,
trailingComma: 'all',
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
bracketSameLine: false,
plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrder: ['<THIRD_PARTY_MODULES>', '^[./](.*)$'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
};
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Custom
.vscode
.DS_Store
dist
.env
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# KaiaFun MCP
> 🐍 ☁️ An MCP server for listing and trading tokens on [KaiaFun](http://kaiafun.io) and interacting with the Kaia blockchain

## 🛠️ MCP Server
### Overview
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide context to Large Language Models (LLMs).
This repository implements an MCP server for the KaiaFun protocol, enabling token listing, trading, and interaction with the Kaia blockchain (e.g. checking token balances of configured wallet).
### Installation
```bash
# Clone the repository
git clone https://github.com/weerofun/kaiafun-mcp
cd kaiafun-mcp
# Install dependencies
yarn
# Build
yarn build
```
The build process will generate output in the directory specified in `tsconfig.json` (`dist` as default) via `tsc`.
To start the MCP Server, you'll need to run `dist/kaiafun-mcp-server.js` (see [#configuration](#configuration) below).
### Configuration
Update your [Claude Desktop](https://claude.ai/download) configuration by updating `claude_desktop_config.json`:
```json
{
"mcpServers": {
"kaiafun": {
"command": "node",
"args": ["/path/to/dist/kaiafun-mcp-server.js"],
"env": {
"PRIVATE_KEY": "0x"
}
},
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
```
- Set `mcpServers.kaiafun.args[0]` to the absolute path of `dist/kaiafun-mcp-server.js`
- Configure `PRIVATE_KEY` with the account's private key for transaction signing
- We also recommend adding [`@modelcontextprotocol/server-puppeteer`](https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer) to your configuration for basic web browsing capabilities
> [!CAUTION]
> PLEASE NOTE that storing private key (`PRIVATE_KEY`) in plaintext is not safe, and this is primarily for development/exploration purposes.
>
> This repo is currently in development, and the contributors in/and the related projects, protocols, and entities are not responsible for any loss of funds, losses, or issues due to the use of this project.
>
> Anyone is free to use this project at their own risk, and contribute to the project by opening issues and pull requests. 💗
## 🛠️ SDK
We are also working on a TypeScript SDK to interact with the KaiaFun protocol. It powers the core functionality of our MCP server and can later be used independently for building custom applications. Source code is located in the `src/sdk` directory.
Currently supported features are as follows:
- ✅ Listing new tokens with predefined metadata
- ✅ Buying and selling tokens with KAIA
Please note that the SDK is also in beta, and features and implementation are subject to change.
## 📄 License
Licensed under the [Apache License 2.0](LICENSE).
Copyright 2025 KaiaFun.
```
--------------------------------------------------------------------------------
/src/sdk/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './client';
```
--------------------------------------------------------------------------------
/src/sdk/constants.ts:
--------------------------------------------------------------------------------
```typescript
export const KAIAFUN_CORE_ADDRESS = '0x080F8b793FE69Fe9e65b5aE17b10F987c95530Bf' as const;
export const WETH_ADDRESS = '0x19Aac5f612f524B754CA7e7c41cbFa2E981A4432' as const;
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@weero/kaiafun-sdk",
"version": "1.0.0",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.8.0",
"axios": "^1.8.4",
"viem": "^2.25.0",
"ws": "^8.18.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.14.0",
"@types/ws": "^8.18.1",
"prettier": "^3.5.3",
"tsc": "^2.0.4",
"typescript": "^5.8.3"
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2021",
"module": "Node16",
"moduleResolution": "Node16",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"baseUrl": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/sdk/utils.ts:
--------------------------------------------------------------------------------
```typescript
import { Abi, ContractEventName, TransactionReceipt, decodeEventLog } from 'viem';
export const getEventFromReceipt = <
const abi extends Abi | readonly unknown[],
eventName extends ContractEventName<abi> | undefined = undefined,
>(
receipt: TransactionReceipt | void,
abi: abi,
eventName: eventName,
) => {
return receipt?.logs
.map((log) => {
try {
return decodeEventLog({ abi, data: log.data, topics: log.topics });
} catch {
return undefined;
}
})
.find((log) => log?.eventName === eventName);
};
```
--------------------------------------------------------------------------------
/src/sdk/chain.ts:
--------------------------------------------------------------------------------
```typescript
import { PublicClient, Transport, createPublicClient, defineChain, http } from 'viem';
export const kaiaMainnet = defineChain({
id: 8217,
name: 'Kaia',
nativeCurrency: {
decimals: 18,
name: 'Kaia',
symbol: 'KAIA',
},
rpcUrls: {
default: {
http: ['https://public-en.node.kaia.io'],
},
},
blockExplorers: {
default: {
name: 'Kaiascope',
url: 'https://klaytnscope.com',
},
},
contracts: {
multicall3: {
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
blockCreated: 96002415,
},
},
});
export const publicClient = createPublicClient({
chain: kaiaMainnet,
transport: http(),
});
```
--------------------------------------------------------------------------------
/src/sdk/abi.ts:
--------------------------------------------------------------------------------
```typescript
export const buyWithETH = {
inputs: [
{ internalType: 'address', name: '_tokenAddress', type: 'address' },
{ internalType: 'uint256', name: '_minTokenAmount', type: 'uint256' },
],
name: 'buyWithETH',
outputs: [],
stateMutability: 'payable',
type: 'function',
} as const;
export const listWithETH = {
inputs: [
{ internalType: 'address', name: '_wethAddress', type: 'address' },
{ internalType: 'string', name: '_name', type: 'string' },
{ internalType: 'string', name: '_symbol', type: 'string' },
{ internalType: 'string', name: '_metadataHash', type: 'string' },
],
name: 'listWithETH',
outputs: [],
stateMutability: 'payable',
type: 'function',
} as const;
export const sell = {
inputs: [
{ internalType: 'address', name: '_tokenAddress', type: 'address' },
{ internalType: 'uint256', name: '_tokenAmount', type: 'uint256' },
{ internalType: 'uint256', name: '_minBaseTokenAmount', type: 'uint256' },
{ internalType: 'bool', name: '_isOutputETH', type: 'bool' },
],
name: 'sell',
outputs: [{ internalType: 'uint256', name: '_baseTokenAfterFee', type: 'uint256' }],
stateMutability: 'nonpayable',
type: 'function',
} as const;
export const TradeEvent = {
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'tokenAddress', type: 'address' },
{ indexed: true, internalType: 'address', name: 'sender', type: 'address' },
{ indexed: false, internalType: 'uint256', name: 'baseIn', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'tokenIn', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'baseOut', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'tokenOut', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'baseFee', type: 'uint256' },
{ indexed: false, internalType: 'address', name: 'instrument', type: 'address' },
],
name: 'Trade',
type: 'event',
} as const;
export const ListEvent = {
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'creator', type: 'address' },
{ indexed: true, internalType: 'address', name: 'tokenAddress', type: 'address' },
{ indexed: true, internalType: 'address', name: 'baseTokenAddress', type: 'address' },
{ indexed: false, internalType: 'string', name: 'name', type: 'string' },
{ indexed: false, internalType: 'string', name: 'symbol', type: 'string' },
{ indexed: false, internalType: 'string', name: 'metadataHash', type: 'string' },
],
name: 'List',
type: 'event',
} as const;
export const ABI = {
// trading
buyWithETH,
sell,
TradeEvent,
// listing
ListEvent,
listWithETH,
};
```
--------------------------------------------------------------------------------
/src/sdk/client.ts:
--------------------------------------------------------------------------------
```typescript
import axios from 'axios';
import {
Account,
Address,
Chain,
PublicClient,
RpcSchema,
Transport,
WalletClient,
createWalletClient,
http,
parseEther,
} from 'viem';
import { waitForTransactionReceipt } from 'viem/actions';
import { ABI } from './abi';
import { publicClient as defaultPublicClient, kaiaMainnet } from './chain';
import { KAIAFUN_CORE_ADDRESS, WETH_ADDRESS } from './constants';
import { getEventFromReceipt } from './utils';
export namespace KaiaFunSchema {
export type Metadata = {
name: string;
symbol: string;
description: string;
imageURL: string;
// optional
twitter?: string;
telegram?: string;
website?: string;
};
export type BuyOptions = {
tokenAddress: Address;
amount: bigint;
minTokenAmount?: bigint;
};
export type SellOptions = {
tokenAddress: Address;
amount: bigint;
minBaseAmount?: bigint;
isOutputKAIA?: boolean;
};
export type ListOptions = {
metadata: Metadata;
};
}
export type KaiaFunClientOptions = {
account: Account;
publicClient?: PublicClient<Transport, Chain>;
walletClient?: WalletClient<Transport, Chain, Account, RpcSchema>;
};
export class KaiaFunClient {
public readonly publicClient: PublicClient<Transport, Chain>;
public readonly walletClient: WalletClient<Transport, Chain, Account, RpcSchema>;
public readonly account: Account;
public readonly API_BASE_URL = 'https://kaiafun.io/api';
constructor({ account, publicClient, walletClient }: KaiaFunClientOptions) {
this.account = account;
this.publicClient = publicClient || defaultPublicClient;
this.walletClient =
walletClient ||
createWalletClient({
account,
chain: kaiaMainnet,
transport: http(),
});
}
public async buy({ tokenAddress, amount, minTokenAmount = 0n }: KaiaFunSchema.BuyOptions) {
if (this.walletClient.chain?.id !== kaiaMainnet.id) {
throw new Error('Unsupported chain');
}
const hash = await this.walletClient.writeContract({
address: KAIAFUN_CORE_ADDRESS,
abi: [ABI.buyWithETH],
functionName: 'buyWithETH',
args: [tokenAddress, minTokenAmount],
value: amount,
});
const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
const tradeEvent = getEventFromReceipt(receipt, [ABI.TradeEvent], ABI.TradeEvent.name);
return { receipt, tradeEvent };
}
public async sell({
tokenAddress,
amount,
minBaseAmount = 0n,
isOutputKAIA = true,
}: KaiaFunSchema.SellOptions) {
if (this.walletClient.chain?.id !== kaiaMainnet.id) {
throw new Error('Unsupported chain');
}
const hash = await this.walletClient.writeContract({
address: KAIAFUN_CORE_ADDRESS,
abi: [ABI.sell],
functionName: 'sell',
args: [tokenAddress, amount, minBaseAmount, isOutputKAIA],
});
const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
const tradeEvent = getEventFromReceipt(receipt, [ABI.TradeEvent], ABI.TradeEvent.name);
return { receipt, tradeEvent };
}
public async list({ metadata }: KaiaFunSchema.ListOptions) {
if (this.walletClient.chain?.id !== kaiaMainnet.id) {
throw new Error('Unsupported chain');
}
// check if user KAIA (ETH) balance is more then 10 KAIA
const balance = await this.publicClient.getBalance({
address: this.walletClient.account.address,
});
if (balance < parseEther('10')) {
throw new Error('Insufficient balance');
}
const serialized = JSON.stringify({
name: metadata.name,
symbol: metadata.symbol,
description: metadata.description,
imageURL: metadata.imageURL,
creator: this.walletClient.account.address.toLowerCase(),
t: Date.now(),
});
const {
data: { hash: metadataHash },
} = await axios.post<{ hash: string }>(`${this.API_BASE_URL}/token/metadata`, {
metadata: serialized,
});
const hash = await this.walletClient.writeContract({
address: KAIAFUN_CORE_ADDRESS,
abi: [ABI.listWithETH],
functionName: 'listWithETH',
args: [WETH_ADDRESS, metadata.name, metadata.symbol, metadataHash],
value: parseEther('10'),
});
const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
const listEvent = getEventFromReceipt(receipt, [ABI.ListEvent], ABI.ListEvent.name);
return { receipt, listEvent };
}
public async uploadImage(file: File): Promise<string | null> {
try {
const formData = new FormData();
formData.append('file', file);
const { data } = await axios.post<{ url: string }>(
`${this.API_BASE_URL}/upload?filename=${file.name}`,
file,
{ headers: { 'Content-Type': file.type } },
);
return data.url;
} catch (error) {
console.error('Error:', error);
return null;
}
}
}
```
--------------------------------------------------------------------------------
/src/kaiafun-mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { Address, Hex, formatEther, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { z } from 'zod';
import { KaiaFunClient, KaiaFunSchema } from './sdk/client';
type KaiaFunServer = Server & {
kaiaFunClient?: KaiaFunClient;
imageCache?: Map<string, { path: string; mimeType: string }>;
};
// Schemas for validation
const schemas = {
toolInputs: {
listMemecoin: z.object({
name: z.string().min(1, 'Name is required'),
symbol: z.string().min(1, 'Symbol is required'),
description: z.string().min(1, 'Description is required'),
imageURL: z
.string()
.url('Image URL must be a valid URL (best if result from `upload_image` tool)'),
twitter: z.string().optional(),
telegram: z.string().optional(),
website: z.string().optional(),
}),
buyMemecoin: z.object({
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
amount: z.string().min(1, 'Amount is required'),
minTokenAmount: z.string().optional(),
}),
sellMemecoin: z.object({
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
amount: z.string().min(1, 'Amount is required'),
minBaseAmount: z.string().optional(),
isOutputKAIA: z.boolean().optional(),
}),
getTokenInfo: z.object({
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
}),
getTokenUrl: z.object({
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
}),
parseEther: z.object({
amount: z.string().min(1, 'Amount is required'),
}),
formatEther: z.object({
amount: z.string().min(1, 'Amount is required'),
}),
getWalletBalance: z.object({}),
getWalletAddress: z.object({}),
getTokenBalance: z.object({
tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
}),
// getTransactionHistory: z.object({
// limit: z.number().optional(),
// }),
uploadImage: z.object({
imageURL: z.string().url('Image URL must be a valid URL'),
}),
},
};
// Setup KaiaFun client
const setupKaiaFunClient = (privateKey: Hex): KaiaFunClient => {
const account = privateKeyToAccount(privateKey);
return new KaiaFunClient({ account });
};
function formatError(error: any): string {
console.error('Full error:', JSON.stringify(error, null, 2));
if (error.code) {
return `Error (${error.code}): ${error.message || 'Unknown error'}`;
}
return error.message || 'An unknown error occurred';
}
const TOOL_DEFINITIONS = [
{
name: 'list_memecoin',
description: 'List a new memecoin on KaiaFun (requires 10 KAIA to list)',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the memecoin',
},
symbol: {
type: 'string',
description: 'Symbol of the memecoin (ticker)',
},
description: {
type: 'string',
description: 'Description of the memecoin',
},
imageURL: {
type: 'string',
description: 'URL to the memecoin image',
},
twitter: {
type: 'string',
description: 'Twitter handle (optional)',
},
telegram: {
type: 'string',
description: 'Telegram group (optional)',
},
website: {
type: 'string',
description: 'Website URL (optional)',
},
},
required: ['name', 'symbol', 'description', 'imageURL'],
},
},
{
name: 'buy_memecoin',
description: 'Buy a memecoin with KAIA',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Address of the token to buy',
},
amount: {
type: 'string',
description: 'Amount of KAIA to spend (in KAIA, not wei)',
},
minTokenAmount: {
type: 'string',
description: 'Minimum amount of tokens to receive (optional)',
},
},
required: ['tokenAddress', 'amount'],
},
},
{
name: 'sell_memecoin',
description: 'Sell a memecoin for KAIA',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Address of the token to sell',
},
amount: {
type: 'string',
description: 'Amount of tokens to sell',
},
minBaseAmount: {
type: 'string',
description: 'Minimum amount of KAIA to receive (optional)',
},
isOutputKAIA: {
type: 'boolean',
description: 'Whether to receive KAIA or WKAIA (optional, defaults to true)',
},
},
required: ['tokenAddress', 'amount'],
},
},
{
name: 'get_token_info',
description: 'Get information about a token',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Address of the token',
},
},
required: ['tokenAddress'],
},
},
{
name: 'get_token_url',
description: 'Get the KaiaFun detail page URL for a token',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Address of the token',
},
},
required: ['tokenAddress'],
},
},
{
name: 'parse_ether',
description: 'Convert KAIA amount to wei (1 KAIA = 10^18 wei)',
inputSchema: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Amount in KAIA',
},
},
required: ['amount'],
},
},
{
name: 'format_ether',
description: 'Convert wei amount to KAIA (10^18 wei = 1 KAIA)',
inputSchema: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Amount in wei',
},
},
required: ['amount'],
},
},
{
name: 'get_wallet_balance',
description: 'Get the KAIA balance of the wallet',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_wallet_address',
description: 'Get the wallet address being used for transactions',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_token_balance',
description: 'Get the balance of a specific token for the wallet',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Address of the token to check balance for',
},
},
required: ['tokenAddress'],
},
},
{
name: 'get_transaction_history',
description: 'Get recent transactions for the wallet (limited functionality)',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of transactions to retrieve (optional)',
},
},
required: [],
},
},
{
name: 'upload_image',
description: 'Upload an image from a URL to KaiaFun server and return the new image URL',
inputSchema: {
type: 'object',
properties: {
imageURL: {
type: 'string',
description: 'URL of the image to upload, preferably from a website (not base64 encoded)',
},
},
required: ['imageURL'],
},
},
] as const;
// Helper function to download image from URL and save to temp file
async function downloadImageToTempFile(
imageUrl: string,
): Promise<{ filePath: string; fileName: string; mimeType: string }> {
try {
// Generate a temp file path
const tempDir = os.tmpdir();
const randomFileName = `${crypto.randomUUID()}.${getExtensionFromUrl(imageUrl)}`;
const tempFilePath = path.join(tempDir, randomFileName);
// Download the image
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer',
});
// Get content type
const mimeType = response.headers['content-type'] || 'image/jpeg';
// Write to temp file
fs.writeFileSync(tempFilePath, Buffer.from(response.data));
return { filePath: tempFilePath, fileName: randomFileName, mimeType };
} catch (error) {
console.error('Error downloading image:', error);
throw new Error('Failed to download image from URL');
}
}
// Extract extension from URL
function getExtensionFromUrl(url: string): string {
try {
const pathname = new URL(url).pathname;
const extension = path.extname(pathname).slice(1);
return extension || 'jpg';
} catch (error) {
return 'jpg';
}
}
// Tool implementation handlers with closure for accessing kaiaFunClient
const createToolHandlers = (server: KaiaFunServer) => ({
async list_memecoin(args: unknown) {
try {
const { name, symbol, description, imageURL, twitter, telegram, website } =
schemas.toolInputs.listMemecoin.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
const metadata: KaiaFunSchema.Metadata = {
name,
symbol,
description,
imageURL,
twitter,
telegram,
website,
};
const result = await server.kaiaFunClient.list({ metadata });
const tokenAddress = result.listEvent?.args.tokenAddress || 'Unknown';
return {
content: [
{
type: 'text',
text: `Successfully listed new memecoin!
Name: ${name}
Symbol: ${symbol}
Token Address: ${tokenAddress}
Transaction Hash: ${result.receipt.transactionHash}`,
},
],
} as const;
} catch (error) {
console.error('Error listing memecoin:', error);
return {
content: [
{
type: 'text',
text: `Error listing memecoin: ${formatError(error)}`,
},
],
} as const;
}
},
async buy_memecoin(args: unknown) {
try {
const { tokenAddress, amount, minTokenAmount } = schemas.toolInputs.buyMemecoin.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
const parsedAmount = parseEther(amount);
const parsedMinTokenAmount = minTokenAmount ? parseEther(minTokenAmount) : 0n;
const result = await server.kaiaFunClient.buy({
tokenAddress: tokenAddress as `0x${string}`,
amount: parsedAmount,
minTokenAmount: parsedMinTokenAmount,
});
return {
content: [
{
type: 'text',
text: `Successfully bought memecoin!
Token Address: ${tokenAddress}
Amount Spent: ${amount} KAIA
Transaction Hash: ${result.receipt.transactionHash}`,
},
],
} as const;
} catch (error) {
console.error('Error buying memecoin:', error);
return {
content: [
{
type: 'text',
text: `Error buying memecoin: ${formatError(error)}`,
},
],
} as const;
}
},
async sell_memecoin(args: unknown) {
try {
const { tokenAddress, amount, minBaseAmount, isOutputKAIA } =
schemas.toolInputs.sellMemecoin.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
const parsedAmount = parseEther(amount);
const parsedMinBaseAmount = minBaseAmount ? parseEther(minBaseAmount) : 0n;
const result = await server.kaiaFunClient.sell({
tokenAddress: tokenAddress as `0x${string}`,
amount: parsedAmount,
minBaseAmount: parsedMinBaseAmount,
isOutputKAIA,
});
return {
content: [
{
type: 'text',
text: `Successfully sold memecoin!
Token Address: ${tokenAddress}
Amount Sold: ${amount} tokens
Transaction Hash: ${result.receipt.transactionHash}`,
},
],
} as const;
} catch (error) {
console.error('Error selling memecoin:', error);
return {
content: [
{
type: 'text',
text: `Error selling memecoin: ${formatError(error)}`,
},
],
} as const;
}
},
async get_token_info(args: unknown) {
try {
const { tokenAddress } = schemas.toolInputs.getTokenInfo.parse(args);
// This is a placeholder - KaiaFunClient doesn't have a method to get token info directly
// We would need to add this functionality or use the publicClient to fetch the data
// For now, let's return a simple response with the address
return {
content: [
{
type: 'text',
text: `Token information for ${tokenAddress}:
This feature is not fully implemented yet.
You can use this token address for buy/sell operations.`,
},
],
} as const;
} catch (error) {
console.error('Error getting token info:', error);
return {
content: [
{
type: 'text',
text: `Error getting token info: ${formatError(error)}`,
},
],
} as const;
}
},
async get_token_url(args: unknown) {
try {
const { tokenAddress } = schemas.toolInputs.getTokenUrl.parse(args);
// Construct the KaiaFun detail page URL directly
const url = `https://kaiafun.io/token/${tokenAddress.toLowerCase()}`;
return {
content: [
{
type: 'text',
text: `Token URL: ${url}`,
},
],
} as const;
} catch (error) {
console.error('Error getting token URL:', error);
return {
content: [
{
type: 'text',
text: `Error getting token URL: ${formatError(error)}`,
},
],
} as const;
}
},
async parse_ether(args: unknown) {
try {
const { amount } = schemas.toolInputs.parseEther.parse(args);
const parsedAmount = parseEther(amount);
return {
content: [
{
type: 'text',
text: `${amount} KAIA = ${parsedAmount.toString()} wei`,
},
],
} as const;
} catch (error) {
console.error('Error parsing ether:', error);
return {
content: [
{
type: 'text',
text: `Error parsing ether: ${formatError(error)}`,
},
],
} as const;
}
},
async format_ether(args: unknown) {
try {
const { amount } = schemas.toolInputs.formatEther.parse(args);
const formattedAmount = formatEther(BigInt(amount));
return {
content: [
{
type: 'text',
text: `${amount} wei = ${formattedAmount} KAIA`,
},
],
} as const;
} catch (error) {
console.error('Error formatting ether:', error);
return {
content: [
{
type: 'text',
text: `Error formatting ether: ${formatError(error)}`,
},
],
} as const;
}
},
async get_wallet_balance(args: unknown) {
try {
// Parse arguments (empty in this case)
schemas.toolInputs.getWalletBalance.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
const balance = await server.kaiaFunClient.publicClient.getBalance({
address: server.kaiaFunClient.account.address,
});
const formattedBalance = formatEther(balance);
return {
content: [
{
type: 'text',
text: `Wallet Balance: ${formattedBalance} KAIA`,
},
],
} as const;
} catch (error) {
console.error('Error getting wallet balance:', error);
return {
content: [
{
type: 'text',
text: `Error getting wallet balance: ${formatError(error)}`,
},
],
} as const;
}
},
async get_wallet_address(args: unknown) {
try {
// Parse arguments (empty in this case)
schemas.toolInputs.getWalletAddress.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
return {
content: [
{
type: 'text',
text: `Wallet Address: ${server.kaiaFunClient.account.address}`,
},
],
} as const;
} catch (error) {
console.error('Error getting wallet address:', error);
return {
content: [
{
type: 'text',
text: `Error getting wallet address: ${formatError(error)}`,
},
],
} as const;
}
},
async get_token_balance(args: unknown) {
try {
const { tokenAddress } = schemas.toolInputs.getTokenBalance.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
// ERC20 Token balanceOf function
const tokenContract = {
address: tokenAddress as Address,
abi: [
{
inputs: [{ name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ name: 'balance', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'decimals',
outputs: [{ name: '', type: 'uint8' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'symbol',
outputs: [{ name: '', type: 'string' }],
stateMutability: 'view',
type: 'function',
},
],
};
try {
// Get token decimals and symbol
const decimals = (await server.kaiaFunClient.publicClient.readContract({
...tokenContract,
functionName: 'decimals',
})) as number;
const symbol = (await server.kaiaFunClient.publicClient.readContract({
...tokenContract,
functionName: 'symbol',
})) as string;
// Get token balance
const balance = (await server.kaiaFunClient.publicClient.readContract({
...tokenContract,
functionName: 'balanceOf',
args: [server.kaiaFunClient.account.address],
})) as bigint;
// Format based on token decimals
const divisor = 10n ** BigInt(decimals);
const formattedBalance = Number(balance) / Number(divisor);
return {
content: [
{
type: 'text',
text: `Token Balance for ${tokenAddress}:
Symbol: ${symbol}
Balance: ${formattedBalance.toString()} ${symbol}`,
},
],
} as const;
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error reading token contract: ${formatError(error)}
This may not be a valid ERC20 token or the contract may be inaccessible.`,
},
],
} as const;
}
} catch (error) {
console.error('Error getting token balance:', error);
return {
content: [
{
type: 'text',
text: `Error getting token balance: ${formatError(error)}`,
},
],
} as const;
}
},
// async get_transaction_history(args: unknown) {
// try {
// const { limit = 5 } = schemas.toolInputs.getTransactionHistory.parse(args);
// if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
// // Note: To get detailed transaction history, you'd typically need to use a block explorer API
// // or archive node. This is a simplified implementation.
// return {
// content: [
// {
// type: 'text',
// text: `Transaction History Feature:
// Wallet Address: ${server.kaiaFunClient.account.address}
// To view detailed transaction history for this address, please visit:
// https://klaytnscope.com/account/${server.kaiaFunClient.account.address}
// Note: Full transaction history requires integration with a blockchain explorer API, which is not implemented in this basic version.`,
// },
// ],
// } as const;
// } catch (error) {
// console.error('Error getting transaction history:', error);
// return {
// content: [
// {
// type: 'text',
// text: `Error getting transaction history: ${formatError(error)}`,
// },
// ],
// } as const;
// }
// },
async upload_image(args: unknown) {
try {
const { imageURL } = schemas.toolInputs.uploadImage.parse(args);
if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
try {
// Download the image from the URL and save to temp file
const { filePath, fileName, mimeType } = await downloadImageToTempFile(imageURL);
// Create a File object from the temp file
const file = new File([fs.readFileSync(filePath)], fileName, { type: mimeType });
// Upload the image using KaiaFun's uploadImage method
const uploadedUrl = await server.kaiaFunClient.uploadImage(file);
if (!uploadedUrl) {
throw new Error('Failed to upload image to KaiaFun server');
}
// Clean up the temp file
fs.unlinkSync(filePath);
return {
content: [
{
type: 'text',
text: `Successfully uploaded image to KaiaFun!
Original URL: ${imageURL}
Uploaded URL: ${uploadedUrl}`,
},
],
} as const;
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error processing image: ${formatError(error)}`,
},
],
} as const;
}
} catch (error) {
console.error('Error uploading image:', error);
return {
content: [
{
type: 'text',
text: `Error uploading image: ${formatError(error)}`,
},
],
} as const;
}
},
});
// Initialize MCP server
const server: KaiaFunServer = new Server(
{
name: 'kaiafun-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
},
);
// Initialize image cache
server.imageCache = new Map();
// Create tool handlers with access to the server
const toolHandlers = createToolHandlers(server);
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
console.error('Tools requested by client');
return { tools: TOOL_DEFINITIONS };
});
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
try {
const handler = toolHandlers[name as keyof typeof toolHandlers];
if (!handler) {
throw new Error(`Unknown tool: ${name}`);
}
return await handler(args);
} catch (error) {
console.error(`Error executing tool ${name}:`, error);
throw error;
}
});
// Register resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
if (!server.imageCache) {
return { resources: [] };
}
const resources = Array.from(server.imageCache.entries()).map(([uri, info]) => ({
uri,
name: `Image: ${path.basename(uri)}`,
description: 'Uploaded image resource',
mimeType: info.mimeType,
}));
return { resources };
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (!server.imageCache || !server.imageCache.has(uri)) {
throw new Error(`Resource not found: ${uri}`);
}
const imageInfo = server.imageCache.get(uri);
if (!imageInfo) {
throw new Error(`Resource not found: ${uri}`);
}
try {
// Read the image file
const imageBuffer = fs.readFileSync(imageInfo.path);
// Return the image as a binary resource
return {
contents: [
{
uri,
mimeType: imageInfo.mimeType,
blob: imageBuffer.toString('base64'),
},
],
};
} catch (error) {
console.error(`Error reading resource ${uri}:`, error);
throw new Error(`Failed to read resource: ${formatError(error)}`);
}
});
// Start the server
async function main() {
if (!process.env.PRIVATE_KEY) {
throw new Error('PRIVATE_KEY environment variable is required');
}
// Initialize `KaiaFunClient` once when server starts and connect to server instance
server.kaiaFunClient = setupKaiaFunClient(process.env.PRIVATE_KEY as Hex);
console.error('KaiaFun client initialized');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('KaiaFun MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
```