# 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
1 | module.exports = {
2 | semi: true,
3 | singleQuote: true,
4 | printWidth: 100,
5 | trailingComma: 'all',
6 |
7 | tabWidth: 2,
8 | useTabs: false,
9 |
10 | bracketSpacing: true,
11 | bracketSameLine: false,
12 |
13 | plugins: ['@trivago/prettier-plugin-sort-imports'],
14 | importOrder: ['<THIRD_PARTY_MODULES>', '^[./](.*)$'],
15 | importOrderSeparation: true,
16 | importOrderSortSpecifiers: true,
17 | };
18 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Custom
2 | .vscode
3 | .DS_Store
4 | dist
5 | .env
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 | .pnpm-debug.log*
15 |
16 | # Diagnostic reports (https://nodejs.org/api/report.html)
17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Directory for instrumented libs generated by jscoverage/JSCover
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 | *.lcov
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (https://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # Snowpack dependency directory (https://snowpack.dev/)
52 | web_modules/
53 |
54 | # TypeScript cache
55 | *.tsbuildinfo
56 |
57 | # Optional npm cache directory
58 | .npm
59 |
60 | # Optional eslint cache
61 | .eslintcache
62 |
63 | # Optional stylelint cache
64 | .stylelintcache
65 |
66 | # Microbundle cache
67 | .rpt2_cache/
68 | .rts2_cache_cjs/
69 | .rts2_cache_es/
70 | .rts2_cache_umd/
71 |
72 | # Optional REPL history
73 | .node_repl_history
74 |
75 | # Output of 'npm pack'
76 | *.tgz
77 |
78 | # Yarn Integrity file
79 | .yarn-integrity
80 |
81 | # dotenv environment variable files
82 | .env
83 | .env.development.local
84 | .env.test.local
85 | .env.production.local
86 | .env.local
87 |
88 | # parcel-bundler cache (https://parceljs.org/)
89 | .cache
90 | .parcel-cache
91 |
92 | # Next.js build output
93 | .next
94 | out
95 |
96 | # Nuxt.js build / generate output
97 | .nuxt
98 | dist
99 |
100 | # Gatsby files
101 | .cache/
102 | # Comment in the public line in if your project uses Gatsby and not Next.js
103 | # https://nextjs.org/blog/next-9-1#public-directory-support
104 | # public
105 |
106 | # vuepress build output
107 | .vuepress/dist
108 |
109 | # vuepress v2.x temp and cache directory
110 | .temp
111 | .cache
112 |
113 | # vitepress build output
114 | **/.vitepress/dist
115 |
116 | # vitepress cache directory
117 | **/.vitepress/cache
118 |
119 | # Docusaurus cache and generated files
120 | .docusaurus
121 |
122 | # Serverless directories
123 | .serverless/
124 |
125 | # FuseBox cache
126 | .fusebox/
127 |
128 | # DynamoDB Local files
129 | .dynamodb/
130 |
131 | # TernJS port file
132 | .tern-port
133 |
134 | # Stores VSCode versions used for testing VSCode extensions
135 | .vscode-test
136 |
137 | # yarn v2
138 | .yarn/cache
139 | .yarn/unplugged
140 | .yarn/build-state.yml
141 | .yarn/install-state.gz
142 | .pnp.*
143 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # KaiaFun MCP
2 |
3 | > 🐍 ☁️ An MCP server for listing and trading tokens on [KaiaFun](http://kaiafun.io) and interacting with the Kaia blockchain
4 |
5 | 
6 |
7 | ## 🛠️ MCP Server
8 |
9 | ### Overview
10 |
11 | The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide context to Large Language Models (LLMs).
12 |
13 | 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).
14 |
15 | ### Installation
16 |
17 | ```bash
18 | # Clone the repository
19 | git clone https://github.com/weerofun/kaiafun-mcp
20 | cd kaiafun-mcp
21 |
22 | # Install dependencies
23 | yarn
24 |
25 | # Build
26 | yarn build
27 | ```
28 |
29 | The build process will generate output in the directory specified in `tsconfig.json` (`dist` as default) via `tsc`.
30 |
31 | To start the MCP Server, you'll need to run `dist/kaiafun-mcp-server.js` (see [#configuration](#configuration) below).
32 |
33 | ### Configuration
34 |
35 | Update your [Claude Desktop](https://claude.ai/download) configuration by updating `claude_desktop_config.json`:
36 |
37 | ```json
38 | {
39 | "mcpServers": {
40 | "kaiafun": {
41 | "command": "node",
42 | "args": ["/path/to/dist/kaiafun-mcp-server.js"],
43 | "env": {
44 | "PRIVATE_KEY": "0x"
45 | }
46 | },
47 | "puppeteer": {
48 | "command": "npx",
49 | "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
50 | }
51 | }
52 | }
53 | ```
54 |
55 | - Set `mcpServers.kaiafun.args[0]` to the absolute path of `dist/kaiafun-mcp-server.js`
56 | - Configure `PRIVATE_KEY` with the account's private key for transaction signing
57 | - We also recommend adding [`@modelcontextprotocol/server-puppeteer`](https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer) to your configuration for basic web browsing capabilities
58 |
59 | > [!CAUTION]
60 | > PLEASE NOTE that storing private key (`PRIVATE_KEY`) in plaintext is not safe, and this is primarily for development/exploration purposes.
61 | >
62 | > 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.
63 | >
64 | > Anyone is free to use this project at their own risk, and contribute to the project by opening issues and pull requests. 💗
65 |
66 | ## 🛠️ SDK
67 |
68 | 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.
69 |
70 | Currently supported features are as follows:
71 |
72 | - ✅ Listing new tokens with predefined metadata
73 | - ✅ Buying and selling tokens with KAIA
74 |
75 | Please note that the SDK is also in beta, and features and implementation are subject to change.
76 |
77 | ## 📄 License
78 |
79 | Licensed under the [Apache License 2.0](LICENSE).
80 |
81 | Copyright 2025 KaiaFun.
82 |
```
--------------------------------------------------------------------------------
/src/sdk/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './client';
2 |
```
--------------------------------------------------------------------------------
/src/sdk/constants.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const KAIAFUN_CORE_ADDRESS = '0x080F8b793FE69Fe9e65b5aE17b10F987c95530Bf' as const;
2 | export const WETH_ADDRESS = '0x19Aac5f612f524B754CA7e7c41cbFa2E981A4432' as const;
3 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@weero/kaiafun-sdk",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "build": "tsc"
6 | },
7 | "dependencies": {
8 | "@modelcontextprotocol/sdk": "^1.8.0",
9 | "axios": "^1.8.4",
10 | "viem": "^2.25.0",
11 | "ws": "^8.18.1",
12 | "zod": "^3.24.2"
13 | },
14 | "devDependencies": {
15 | "@trivago/prettier-plugin-sort-imports": "^5.2.2",
16 | "@types/node": "^22.14.0",
17 | "@types/ws": "^8.18.1",
18 | "prettier": "^3.5.3",
19 | "tsc": "^2.0.4",
20 | "typescript": "^5.8.3"
21 | }
22 | }
23 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "sourceMap": true,
9 | "outDir": "./dist",
10 | "strict": true,
11 | "strictNullChecks": true,
12 | "esModuleInterop": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "skipLibCheck": true,
17 | "baseUrl": "src"
18 | },
19 | "include": ["src/**/*"],
20 | "exclude": ["node_modules", "dist"]
21 | }
22 |
```
--------------------------------------------------------------------------------
/src/sdk/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Abi, ContractEventName, TransactionReceipt, decodeEventLog } from 'viem';
2 |
3 | export const getEventFromReceipt = <
4 | const abi extends Abi | readonly unknown[],
5 | eventName extends ContractEventName<abi> | undefined = undefined,
6 | >(
7 | receipt: TransactionReceipt | void,
8 | abi: abi,
9 | eventName: eventName,
10 | ) => {
11 | return receipt?.logs
12 | .map((log) => {
13 | try {
14 | return decodeEventLog({ abi, data: log.data, topics: log.topics });
15 | } catch {
16 | return undefined;
17 | }
18 | })
19 | .find((log) => log?.eventName === eventName);
20 | };
21 |
```
--------------------------------------------------------------------------------
/src/sdk/chain.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { PublicClient, Transport, createPublicClient, defineChain, http } from 'viem';
2 |
3 | export const kaiaMainnet = defineChain({
4 | id: 8217,
5 | name: 'Kaia',
6 | nativeCurrency: {
7 | decimals: 18,
8 | name: 'Kaia',
9 | symbol: 'KAIA',
10 | },
11 | rpcUrls: {
12 | default: {
13 | http: ['https://public-en.node.kaia.io'],
14 | },
15 | },
16 | blockExplorers: {
17 | default: {
18 | name: 'Kaiascope',
19 | url: 'https://klaytnscope.com',
20 | },
21 | },
22 | contracts: {
23 | multicall3: {
24 | address: '0xcA11bde05977b3631167028862bE2a173976CA11',
25 | blockCreated: 96002415,
26 | },
27 | },
28 | });
29 |
30 | export const publicClient = createPublicClient({
31 | chain: kaiaMainnet,
32 | transport: http(),
33 | });
34 |
```
--------------------------------------------------------------------------------
/src/sdk/abi.ts:
--------------------------------------------------------------------------------
```typescript
1 | export const buyWithETH = {
2 | inputs: [
3 | { internalType: 'address', name: '_tokenAddress', type: 'address' },
4 | { internalType: 'uint256', name: '_minTokenAmount', type: 'uint256' },
5 | ],
6 | name: 'buyWithETH',
7 | outputs: [],
8 | stateMutability: 'payable',
9 | type: 'function',
10 | } as const;
11 |
12 | export const listWithETH = {
13 | inputs: [
14 | { internalType: 'address', name: '_wethAddress', type: 'address' },
15 | { internalType: 'string', name: '_name', type: 'string' },
16 | { internalType: 'string', name: '_symbol', type: 'string' },
17 | { internalType: 'string', name: '_metadataHash', type: 'string' },
18 | ],
19 | name: 'listWithETH',
20 | outputs: [],
21 | stateMutability: 'payable',
22 | type: 'function',
23 | } as const;
24 |
25 | export const sell = {
26 | inputs: [
27 | { internalType: 'address', name: '_tokenAddress', type: 'address' },
28 | { internalType: 'uint256', name: '_tokenAmount', type: 'uint256' },
29 | { internalType: 'uint256', name: '_minBaseTokenAmount', type: 'uint256' },
30 | { internalType: 'bool', name: '_isOutputETH', type: 'bool' },
31 | ],
32 | name: 'sell',
33 | outputs: [{ internalType: 'uint256', name: '_baseTokenAfterFee', type: 'uint256' }],
34 | stateMutability: 'nonpayable',
35 | type: 'function',
36 | } as const;
37 |
38 | export const TradeEvent = {
39 | anonymous: false,
40 | inputs: [
41 | { indexed: true, internalType: 'address', name: 'tokenAddress', type: 'address' },
42 | { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
43 | { indexed: false, internalType: 'uint256', name: 'baseIn', type: 'uint256' },
44 | { indexed: false, internalType: 'uint256', name: 'tokenIn', type: 'uint256' },
45 | { indexed: false, internalType: 'uint256', name: 'baseOut', type: 'uint256' },
46 | { indexed: false, internalType: 'uint256', name: 'tokenOut', type: 'uint256' },
47 | { indexed: false, internalType: 'uint256', name: 'baseFee', type: 'uint256' },
48 | { indexed: false, internalType: 'address', name: 'instrument', type: 'address' },
49 | ],
50 | name: 'Trade',
51 | type: 'event',
52 | } as const;
53 |
54 | export const ListEvent = {
55 | anonymous: false,
56 | inputs: [
57 | { indexed: true, internalType: 'address', name: 'creator', type: 'address' },
58 | { indexed: true, internalType: 'address', name: 'tokenAddress', type: 'address' },
59 | { indexed: true, internalType: 'address', name: 'baseTokenAddress', type: 'address' },
60 | { indexed: false, internalType: 'string', name: 'name', type: 'string' },
61 | { indexed: false, internalType: 'string', name: 'symbol', type: 'string' },
62 | { indexed: false, internalType: 'string', name: 'metadataHash', type: 'string' },
63 | ],
64 | name: 'List',
65 | type: 'event',
66 | } as const;
67 |
68 | export const ABI = {
69 | // trading
70 | buyWithETH,
71 | sell,
72 | TradeEvent,
73 |
74 | // listing
75 | ListEvent,
76 | listWithETH,
77 | };
78 |
```
--------------------------------------------------------------------------------
/src/sdk/client.ts:
--------------------------------------------------------------------------------
```typescript
1 | import axios from 'axios';
2 | import {
3 | Account,
4 | Address,
5 | Chain,
6 | PublicClient,
7 | RpcSchema,
8 | Transport,
9 | WalletClient,
10 | createWalletClient,
11 | http,
12 | parseEther,
13 | } from 'viem';
14 | import { waitForTransactionReceipt } from 'viem/actions';
15 |
16 | import { ABI } from './abi';
17 | import { publicClient as defaultPublicClient, kaiaMainnet } from './chain';
18 | import { KAIAFUN_CORE_ADDRESS, WETH_ADDRESS } from './constants';
19 | import { getEventFromReceipt } from './utils';
20 |
21 | export namespace KaiaFunSchema {
22 | export type Metadata = {
23 | name: string;
24 | symbol: string;
25 | description: string;
26 | imageURL: string;
27 |
28 | // optional
29 | twitter?: string;
30 | telegram?: string;
31 | website?: string;
32 | };
33 |
34 | export type BuyOptions = {
35 | tokenAddress: Address;
36 | amount: bigint;
37 | minTokenAmount?: bigint;
38 | };
39 | export type SellOptions = {
40 | tokenAddress: Address;
41 | amount: bigint;
42 | minBaseAmount?: bigint;
43 | isOutputKAIA?: boolean;
44 | };
45 | export type ListOptions = {
46 | metadata: Metadata;
47 | };
48 | }
49 |
50 | export type KaiaFunClientOptions = {
51 | account: Account;
52 | publicClient?: PublicClient<Transport, Chain>;
53 | walletClient?: WalletClient<Transport, Chain, Account, RpcSchema>;
54 | };
55 |
56 | export class KaiaFunClient {
57 | public readonly publicClient: PublicClient<Transport, Chain>;
58 | public readonly walletClient: WalletClient<Transport, Chain, Account, RpcSchema>;
59 | public readonly account: Account;
60 | public readonly API_BASE_URL = 'https://kaiafun.io/api';
61 |
62 | constructor({ account, publicClient, walletClient }: KaiaFunClientOptions) {
63 | this.account = account;
64 | this.publicClient = publicClient || defaultPublicClient;
65 | this.walletClient =
66 | walletClient ||
67 | createWalletClient({
68 | account,
69 | chain: kaiaMainnet,
70 | transport: http(),
71 | });
72 | }
73 |
74 | public async buy({ tokenAddress, amount, minTokenAmount = 0n }: KaiaFunSchema.BuyOptions) {
75 | if (this.walletClient.chain?.id !== kaiaMainnet.id) {
76 | throw new Error('Unsupported chain');
77 | }
78 |
79 | const hash = await this.walletClient.writeContract({
80 | address: KAIAFUN_CORE_ADDRESS,
81 | abi: [ABI.buyWithETH],
82 | functionName: 'buyWithETH',
83 | args: [tokenAddress, minTokenAmount],
84 | value: amount,
85 | });
86 |
87 | const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
88 | const tradeEvent = getEventFromReceipt(receipt, [ABI.TradeEvent], ABI.TradeEvent.name);
89 | return { receipt, tradeEvent };
90 | }
91 |
92 | public async sell({
93 | tokenAddress,
94 | amount,
95 | minBaseAmount = 0n,
96 | isOutputKAIA = true,
97 | }: KaiaFunSchema.SellOptions) {
98 | if (this.walletClient.chain?.id !== kaiaMainnet.id) {
99 | throw new Error('Unsupported chain');
100 | }
101 |
102 | const hash = await this.walletClient.writeContract({
103 | address: KAIAFUN_CORE_ADDRESS,
104 | abi: [ABI.sell],
105 | functionName: 'sell',
106 | args: [tokenAddress, amount, minBaseAmount, isOutputKAIA],
107 | });
108 |
109 | const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
110 | const tradeEvent = getEventFromReceipt(receipt, [ABI.TradeEvent], ABI.TradeEvent.name);
111 | return { receipt, tradeEvent };
112 | }
113 |
114 | public async list({ metadata }: KaiaFunSchema.ListOptions) {
115 | if (this.walletClient.chain?.id !== kaiaMainnet.id) {
116 | throw new Error('Unsupported chain');
117 | }
118 |
119 | // check if user KAIA (ETH) balance is more then 10 KAIA
120 | const balance = await this.publicClient.getBalance({
121 | address: this.walletClient.account.address,
122 | });
123 | if (balance < parseEther('10')) {
124 | throw new Error('Insufficient balance');
125 | }
126 |
127 | const serialized = JSON.stringify({
128 | name: metadata.name,
129 | symbol: metadata.symbol,
130 | description: metadata.description,
131 | imageURL: metadata.imageURL,
132 | creator: this.walletClient.account.address.toLowerCase(),
133 | t: Date.now(),
134 | });
135 | const {
136 | data: { hash: metadataHash },
137 | } = await axios.post<{ hash: string }>(`${this.API_BASE_URL}/token/metadata`, {
138 | metadata: serialized,
139 | });
140 |
141 | const hash = await this.walletClient.writeContract({
142 | address: KAIAFUN_CORE_ADDRESS,
143 | abi: [ABI.listWithETH],
144 | functionName: 'listWithETH',
145 | args: [WETH_ADDRESS, metadata.name, metadata.symbol, metadataHash],
146 | value: parseEther('10'),
147 | });
148 |
149 | const receipt = await waitForTransactionReceipt(this.publicClient, { hash });
150 | const listEvent = getEventFromReceipt(receipt, [ABI.ListEvent], ABI.ListEvent.name);
151 | return { receipt, listEvent };
152 | }
153 |
154 | public async uploadImage(file: File): Promise<string | null> {
155 | try {
156 | const formData = new FormData();
157 | formData.append('file', file);
158 |
159 | const { data } = await axios.post<{ url: string }>(
160 | `${this.API_BASE_URL}/upload?filename=${file.name}`,
161 | file,
162 | { headers: { 'Content-Type': file.type } },
163 | );
164 |
165 | return data.url;
166 | } catch (error) {
167 | console.error('Error:', error);
168 | return null;
169 | }
170 | }
171 | }
172 |
```
--------------------------------------------------------------------------------
/src/kaiafun-mcp-server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | import {
4 | CallToolRequestSchema,
5 | ListResourcesRequestSchema,
6 | ListToolsRequestSchema,
7 | ReadResourceRequestSchema,
8 | } from '@modelcontextprotocol/sdk/types.js';
9 | import axios from 'axios';
10 | import * as crypto from 'crypto';
11 | import * as fs from 'fs';
12 | import * as os from 'os';
13 | import * as path from 'path';
14 | import { Address, Hex, formatEther, parseEther } from 'viem';
15 | import { privateKeyToAccount } from 'viem/accounts';
16 | import { z } from 'zod';
17 |
18 | import { KaiaFunClient, KaiaFunSchema } from './sdk/client';
19 |
20 | type KaiaFunServer = Server & {
21 | kaiaFunClient?: KaiaFunClient;
22 | imageCache?: Map<string, { path: string; mimeType: string }>;
23 | };
24 |
25 | // Schemas for validation
26 | const schemas = {
27 | toolInputs: {
28 | listMemecoin: z.object({
29 | name: z.string().min(1, 'Name is required'),
30 | symbol: z.string().min(1, 'Symbol is required'),
31 | description: z.string().min(1, 'Description is required'),
32 | imageURL: z
33 | .string()
34 | .url('Image URL must be a valid URL (best if result from `upload_image` tool)'),
35 | twitter: z.string().optional(),
36 | telegram: z.string().optional(),
37 | website: z.string().optional(),
38 | }),
39 | buyMemecoin: z.object({
40 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
41 | amount: z.string().min(1, 'Amount is required'),
42 | minTokenAmount: z.string().optional(),
43 | }),
44 | sellMemecoin: z.object({
45 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
46 | amount: z.string().min(1, 'Amount is required'),
47 | minBaseAmount: z.string().optional(),
48 | isOutputKAIA: z.boolean().optional(),
49 | }),
50 | getTokenInfo: z.object({
51 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
52 | }),
53 | getTokenUrl: z.object({
54 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
55 | }),
56 | parseEther: z.object({
57 | amount: z.string().min(1, 'Amount is required'),
58 | }),
59 | formatEther: z.object({
60 | amount: z.string().min(1, 'Amount is required'),
61 | }),
62 | getWalletBalance: z.object({}),
63 | getWalletAddress: z.object({}),
64 | getTokenBalance: z.object({
65 | tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid token address'),
66 | }),
67 | // getTransactionHistory: z.object({
68 | // limit: z.number().optional(),
69 | // }),
70 | uploadImage: z.object({
71 | imageURL: z.string().url('Image URL must be a valid URL'),
72 | }),
73 | },
74 | };
75 |
76 | // Setup KaiaFun client
77 | const setupKaiaFunClient = (privateKey: Hex): KaiaFunClient => {
78 | const account = privateKeyToAccount(privateKey);
79 | return new KaiaFunClient({ account });
80 | };
81 |
82 | function formatError(error: any): string {
83 | console.error('Full error:', JSON.stringify(error, null, 2));
84 |
85 | if (error.code) {
86 | return `Error (${error.code}): ${error.message || 'Unknown error'}`;
87 | }
88 | return error.message || 'An unknown error occurred';
89 | }
90 |
91 | const TOOL_DEFINITIONS = [
92 | {
93 | name: 'list_memecoin',
94 | description: 'List a new memecoin on KaiaFun (requires 10 KAIA to list)',
95 | inputSchema: {
96 | type: 'object',
97 | properties: {
98 | name: {
99 | type: 'string',
100 | description: 'Name of the memecoin',
101 | },
102 | symbol: {
103 | type: 'string',
104 | description: 'Symbol of the memecoin (ticker)',
105 | },
106 | description: {
107 | type: 'string',
108 | description: 'Description of the memecoin',
109 | },
110 | imageURL: {
111 | type: 'string',
112 | description: 'URL to the memecoin image',
113 | },
114 | twitter: {
115 | type: 'string',
116 | description: 'Twitter handle (optional)',
117 | },
118 | telegram: {
119 | type: 'string',
120 | description: 'Telegram group (optional)',
121 | },
122 | website: {
123 | type: 'string',
124 | description: 'Website URL (optional)',
125 | },
126 | },
127 | required: ['name', 'symbol', 'description', 'imageURL'],
128 | },
129 | },
130 | {
131 | name: 'buy_memecoin',
132 | description: 'Buy a memecoin with KAIA',
133 | inputSchema: {
134 | type: 'object',
135 | properties: {
136 | tokenAddress: {
137 | type: 'string',
138 | description: 'Address of the token to buy',
139 | },
140 | amount: {
141 | type: 'string',
142 | description: 'Amount of KAIA to spend (in KAIA, not wei)',
143 | },
144 | minTokenAmount: {
145 | type: 'string',
146 | description: 'Minimum amount of tokens to receive (optional)',
147 | },
148 | },
149 | required: ['tokenAddress', 'amount'],
150 | },
151 | },
152 | {
153 | name: 'sell_memecoin',
154 | description: 'Sell a memecoin for KAIA',
155 | inputSchema: {
156 | type: 'object',
157 | properties: {
158 | tokenAddress: {
159 | type: 'string',
160 | description: 'Address of the token to sell',
161 | },
162 | amount: {
163 | type: 'string',
164 | description: 'Amount of tokens to sell',
165 | },
166 | minBaseAmount: {
167 | type: 'string',
168 | description: 'Minimum amount of KAIA to receive (optional)',
169 | },
170 | isOutputKAIA: {
171 | type: 'boolean',
172 | description: 'Whether to receive KAIA or WKAIA (optional, defaults to true)',
173 | },
174 | },
175 | required: ['tokenAddress', 'amount'],
176 | },
177 | },
178 | {
179 | name: 'get_token_info',
180 | description: 'Get information about a token',
181 | inputSchema: {
182 | type: 'object',
183 | properties: {
184 | tokenAddress: {
185 | type: 'string',
186 | description: 'Address of the token',
187 | },
188 | },
189 | required: ['tokenAddress'],
190 | },
191 | },
192 | {
193 | name: 'get_token_url',
194 | description: 'Get the KaiaFun detail page URL for a token',
195 | inputSchema: {
196 | type: 'object',
197 | properties: {
198 | tokenAddress: {
199 | type: 'string',
200 | description: 'Address of the token',
201 | },
202 | },
203 | required: ['tokenAddress'],
204 | },
205 | },
206 | {
207 | name: 'parse_ether',
208 | description: 'Convert KAIA amount to wei (1 KAIA = 10^18 wei)',
209 | inputSchema: {
210 | type: 'object',
211 | properties: {
212 | amount: {
213 | type: 'string',
214 | description: 'Amount in KAIA',
215 | },
216 | },
217 | required: ['amount'],
218 | },
219 | },
220 | {
221 | name: 'format_ether',
222 | description: 'Convert wei amount to KAIA (10^18 wei = 1 KAIA)',
223 | inputSchema: {
224 | type: 'object',
225 | properties: {
226 | amount: {
227 | type: 'string',
228 | description: 'Amount in wei',
229 | },
230 | },
231 | required: ['amount'],
232 | },
233 | },
234 | {
235 | name: 'get_wallet_balance',
236 | description: 'Get the KAIA balance of the wallet',
237 | inputSchema: {
238 | type: 'object',
239 | properties: {},
240 | required: [],
241 | },
242 | },
243 | {
244 | name: 'get_wallet_address',
245 | description: 'Get the wallet address being used for transactions',
246 | inputSchema: {
247 | type: 'object',
248 | properties: {},
249 | required: [],
250 | },
251 | },
252 | {
253 | name: 'get_token_balance',
254 | description: 'Get the balance of a specific token for the wallet',
255 | inputSchema: {
256 | type: 'object',
257 | properties: {
258 | tokenAddress: {
259 | type: 'string',
260 | description: 'Address of the token to check balance for',
261 | },
262 | },
263 | required: ['tokenAddress'],
264 | },
265 | },
266 | {
267 | name: 'get_transaction_history',
268 | description: 'Get recent transactions for the wallet (limited functionality)',
269 | inputSchema: {
270 | type: 'object',
271 | properties: {
272 | limit: {
273 | type: 'number',
274 | description: 'Maximum number of transactions to retrieve (optional)',
275 | },
276 | },
277 | required: [],
278 | },
279 | },
280 | {
281 | name: 'upload_image',
282 | description: 'Upload an image from a URL to KaiaFun server and return the new image URL',
283 | inputSchema: {
284 | type: 'object',
285 | properties: {
286 | imageURL: {
287 | type: 'string',
288 | description: 'URL of the image to upload, preferably from a website (not base64 encoded)',
289 | },
290 | },
291 | required: ['imageURL'],
292 | },
293 | },
294 | ] as const;
295 |
296 | // Helper function to download image from URL and save to temp file
297 | async function downloadImageToTempFile(
298 | imageUrl: string,
299 | ): Promise<{ filePath: string; fileName: string; mimeType: string }> {
300 | try {
301 | // Generate a temp file path
302 | const tempDir = os.tmpdir();
303 | const randomFileName = `${crypto.randomUUID()}.${getExtensionFromUrl(imageUrl)}`;
304 | const tempFilePath = path.join(tempDir, randomFileName);
305 |
306 | // Download the image
307 | const response = await axios.get(imageUrl, {
308 | responseType: 'arraybuffer',
309 | });
310 |
311 | // Get content type
312 | const mimeType = response.headers['content-type'] || 'image/jpeg';
313 |
314 | // Write to temp file
315 | fs.writeFileSync(tempFilePath, Buffer.from(response.data));
316 |
317 | return { filePath: tempFilePath, fileName: randomFileName, mimeType };
318 | } catch (error) {
319 | console.error('Error downloading image:', error);
320 | throw new Error('Failed to download image from URL');
321 | }
322 | }
323 |
324 | // Extract extension from URL
325 | function getExtensionFromUrl(url: string): string {
326 | try {
327 | const pathname = new URL(url).pathname;
328 | const extension = path.extname(pathname).slice(1);
329 | return extension || 'jpg';
330 | } catch (error) {
331 | return 'jpg';
332 | }
333 | }
334 |
335 | // Tool implementation handlers with closure for accessing kaiaFunClient
336 | const createToolHandlers = (server: KaiaFunServer) => ({
337 | async list_memecoin(args: unknown) {
338 | try {
339 | const { name, symbol, description, imageURL, twitter, telegram, website } =
340 | schemas.toolInputs.listMemecoin.parse(args);
341 |
342 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
343 |
344 | const metadata: KaiaFunSchema.Metadata = {
345 | name,
346 | symbol,
347 | description,
348 | imageURL,
349 | twitter,
350 | telegram,
351 | website,
352 | };
353 |
354 | const result = await server.kaiaFunClient.list({ metadata });
355 |
356 | const tokenAddress = result.listEvent?.args.tokenAddress || 'Unknown';
357 |
358 | return {
359 | content: [
360 | {
361 | type: 'text',
362 | text: `Successfully listed new memecoin!
363 | Name: ${name}
364 | Symbol: ${symbol}
365 | Token Address: ${tokenAddress}
366 | Transaction Hash: ${result.receipt.transactionHash}`,
367 | },
368 | ],
369 | } as const;
370 | } catch (error) {
371 | console.error('Error listing memecoin:', error);
372 | return {
373 | content: [
374 | {
375 | type: 'text',
376 | text: `Error listing memecoin: ${formatError(error)}`,
377 | },
378 | ],
379 | } as const;
380 | }
381 | },
382 |
383 | async buy_memecoin(args: unknown) {
384 | try {
385 | const { tokenAddress, amount, minTokenAmount } = schemas.toolInputs.buyMemecoin.parse(args);
386 |
387 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
388 |
389 | const parsedAmount = parseEther(amount);
390 | const parsedMinTokenAmount = minTokenAmount ? parseEther(minTokenAmount) : 0n;
391 |
392 | const result = await server.kaiaFunClient.buy({
393 | tokenAddress: tokenAddress as `0x${string}`,
394 | amount: parsedAmount,
395 | minTokenAmount: parsedMinTokenAmount,
396 | });
397 |
398 | return {
399 | content: [
400 | {
401 | type: 'text',
402 | text: `Successfully bought memecoin!
403 | Token Address: ${tokenAddress}
404 | Amount Spent: ${amount} KAIA
405 | Transaction Hash: ${result.receipt.transactionHash}`,
406 | },
407 | ],
408 | } as const;
409 | } catch (error) {
410 | console.error('Error buying memecoin:', error);
411 | return {
412 | content: [
413 | {
414 | type: 'text',
415 | text: `Error buying memecoin: ${formatError(error)}`,
416 | },
417 | ],
418 | } as const;
419 | }
420 | },
421 |
422 | async sell_memecoin(args: unknown) {
423 | try {
424 | const { tokenAddress, amount, minBaseAmount, isOutputKAIA } =
425 | schemas.toolInputs.sellMemecoin.parse(args);
426 |
427 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
428 |
429 | const parsedAmount = parseEther(amount);
430 | const parsedMinBaseAmount = minBaseAmount ? parseEther(minBaseAmount) : 0n;
431 |
432 | const result = await server.kaiaFunClient.sell({
433 | tokenAddress: tokenAddress as `0x${string}`,
434 | amount: parsedAmount,
435 | minBaseAmount: parsedMinBaseAmount,
436 | isOutputKAIA,
437 | });
438 |
439 | return {
440 | content: [
441 | {
442 | type: 'text',
443 | text: `Successfully sold memecoin!
444 | Token Address: ${tokenAddress}
445 | Amount Sold: ${amount} tokens
446 | Transaction Hash: ${result.receipt.transactionHash}`,
447 | },
448 | ],
449 | } as const;
450 | } catch (error) {
451 | console.error('Error selling memecoin:', error);
452 | return {
453 | content: [
454 | {
455 | type: 'text',
456 | text: `Error selling memecoin: ${formatError(error)}`,
457 | },
458 | ],
459 | } as const;
460 | }
461 | },
462 |
463 | async get_token_info(args: unknown) {
464 | try {
465 | const { tokenAddress } = schemas.toolInputs.getTokenInfo.parse(args);
466 |
467 | // This is a placeholder - KaiaFunClient doesn't have a method to get token info directly
468 | // We would need to add this functionality or use the publicClient to fetch the data
469 |
470 | // For now, let's return a simple response with the address
471 | return {
472 | content: [
473 | {
474 | type: 'text',
475 | text: `Token information for ${tokenAddress}:
476 | This feature is not fully implemented yet.
477 | You can use this token address for buy/sell operations.`,
478 | },
479 | ],
480 | } as const;
481 | } catch (error) {
482 | console.error('Error getting token info:', error);
483 | return {
484 | content: [
485 | {
486 | type: 'text',
487 | text: `Error getting token info: ${formatError(error)}`,
488 | },
489 | ],
490 | } as const;
491 | }
492 | },
493 |
494 | async get_token_url(args: unknown) {
495 | try {
496 | const { tokenAddress } = schemas.toolInputs.getTokenUrl.parse(args);
497 |
498 | // Construct the KaiaFun detail page URL directly
499 | const url = `https://kaiafun.io/token/${tokenAddress.toLowerCase()}`;
500 |
501 | return {
502 | content: [
503 | {
504 | type: 'text',
505 | text: `Token URL: ${url}`,
506 | },
507 | ],
508 | } as const;
509 | } catch (error) {
510 | console.error('Error getting token URL:', error);
511 | return {
512 | content: [
513 | {
514 | type: 'text',
515 | text: `Error getting token URL: ${formatError(error)}`,
516 | },
517 | ],
518 | } as const;
519 | }
520 | },
521 |
522 | async parse_ether(args: unknown) {
523 | try {
524 | const { amount } = schemas.toolInputs.parseEther.parse(args);
525 |
526 | const parsedAmount = parseEther(amount);
527 |
528 | return {
529 | content: [
530 | {
531 | type: 'text',
532 | text: `${amount} KAIA = ${parsedAmount.toString()} wei`,
533 | },
534 | ],
535 | } as const;
536 | } catch (error) {
537 | console.error('Error parsing ether:', error);
538 | return {
539 | content: [
540 | {
541 | type: 'text',
542 | text: `Error parsing ether: ${formatError(error)}`,
543 | },
544 | ],
545 | } as const;
546 | }
547 | },
548 |
549 | async format_ether(args: unknown) {
550 | try {
551 | const { amount } = schemas.toolInputs.formatEther.parse(args);
552 |
553 | const formattedAmount = formatEther(BigInt(amount));
554 |
555 | return {
556 | content: [
557 | {
558 | type: 'text',
559 | text: `${amount} wei = ${formattedAmount} KAIA`,
560 | },
561 | ],
562 | } as const;
563 | } catch (error) {
564 | console.error('Error formatting ether:', error);
565 | return {
566 | content: [
567 | {
568 | type: 'text',
569 | text: `Error formatting ether: ${formatError(error)}`,
570 | },
571 | ],
572 | } as const;
573 | }
574 | },
575 |
576 | async get_wallet_balance(args: unknown) {
577 | try {
578 | // Parse arguments (empty in this case)
579 | schemas.toolInputs.getWalletBalance.parse(args);
580 |
581 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
582 |
583 | const balance = await server.kaiaFunClient.publicClient.getBalance({
584 | address: server.kaiaFunClient.account.address,
585 | });
586 |
587 | const formattedBalance = formatEther(balance);
588 |
589 | return {
590 | content: [
591 | {
592 | type: 'text',
593 | text: `Wallet Balance: ${formattedBalance} KAIA`,
594 | },
595 | ],
596 | } as const;
597 | } catch (error) {
598 | console.error('Error getting wallet balance:', error);
599 | return {
600 | content: [
601 | {
602 | type: 'text',
603 | text: `Error getting wallet balance: ${formatError(error)}`,
604 | },
605 | ],
606 | } as const;
607 | }
608 | },
609 |
610 | async get_wallet_address(args: unknown) {
611 | try {
612 | // Parse arguments (empty in this case)
613 | schemas.toolInputs.getWalletAddress.parse(args);
614 |
615 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
616 |
617 | return {
618 | content: [
619 | {
620 | type: 'text',
621 | text: `Wallet Address: ${server.kaiaFunClient.account.address}`,
622 | },
623 | ],
624 | } as const;
625 | } catch (error) {
626 | console.error('Error getting wallet address:', error);
627 | return {
628 | content: [
629 | {
630 | type: 'text',
631 | text: `Error getting wallet address: ${formatError(error)}`,
632 | },
633 | ],
634 | } as const;
635 | }
636 | },
637 |
638 | async get_token_balance(args: unknown) {
639 | try {
640 | const { tokenAddress } = schemas.toolInputs.getTokenBalance.parse(args);
641 |
642 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
643 |
644 | // ERC20 Token balanceOf function
645 | const tokenContract = {
646 | address: tokenAddress as Address,
647 | abi: [
648 | {
649 | inputs: [{ name: 'account', type: 'address' }],
650 | name: 'balanceOf',
651 | outputs: [{ name: 'balance', type: 'uint256' }],
652 | stateMutability: 'view',
653 | type: 'function',
654 | },
655 | {
656 | inputs: [],
657 | name: 'decimals',
658 | outputs: [{ name: '', type: 'uint8' }],
659 | stateMutability: 'view',
660 | type: 'function',
661 | },
662 | {
663 | inputs: [],
664 | name: 'symbol',
665 | outputs: [{ name: '', type: 'string' }],
666 | stateMutability: 'view',
667 | type: 'function',
668 | },
669 | ],
670 | };
671 |
672 | try {
673 | // Get token decimals and symbol
674 | const decimals = (await server.kaiaFunClient.publicClient.readContract({
675 | ...tokenContract,
676 | functionName: 'decimals',
677 | })) as number;
678 |
679 | const symbol = (await server.kaiaFunClient.publicClient.readContract({
680 | ...tokenContract,
681 | functionName: 'symbol',
682 | })) as string;
683 |
684 | // Get token balance
685 | const balance = (await server.kaiaFunClient.publicClient.readContract({
686 | ...tokenContract,
687 | functionName: 'balanceOf',
688 | args: [server.kaiaFunClient.account.address],
689 | })) as bigint;
690 |
691 | // Format based on token decimals
692 | const divisor = 10n ** BigInt(decimals);
693 | const formattedBalance = Number(balance) / Number(divisor);
694 |
695 | return {
696 | content: [
697 | {
698 | type: 'text',
699 | text: `Token Balance for ${tokenAddress}:
700 | Symbol: ${symbol}
701 | Balance: ${formattedBalance.toString()} ${symbol}`,
702 | },
703 | ],
704 | } as const;
705 | } catch (error) {
706 | return {
707 | content: [
708 | {
709 | type: 'text',
710 | text: `Error reading token contract: ${formatError(error)}
711 | This may not be a valid ERC20 token or the contract may be inaccessible.`,
712 | },
713 | ],
714 | } as const;
715 | }
716 | } catch (error) {
717 | console.error('Error getting token balance:', error);
718 | return {
719 | content: [
720 | {
721 | type: 'text',
722 | text: `Error getting token balance: ${formatError(error)}`,
723 | },
724 | ],
725 | } as const;
726 | }
727 | },
728 |
729 | // async get_transaction_history(args: unknown) {
730 | // try {
731 | // const { limit = 5 } = schemas.toolInputs.getTransactionHistory.parse(args);
732 |
733 | // if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
734 |
735 | // // Note: To get detailed transaction history, you'd typically need to use a block explorer API
736 | // // or archive node. This is a simplified implementation.
737 |
738 | // return {
739 | // content: [
740 | // {
741 | // type: 'text',
742 | // text: `Transaction History Feature:
743 | // Wallet Address: ${server.kaiaFunClient.account.address}
744 |
745 | // To view detailed transaction history for this address, please visit:
746 | // https://klaytnscope.com/account/${server.kaiaFunClient.account.address}
747 |
748 | // Note: Full transaction history requires integration with a blockchain explorer API, which is not implemented in this basic version.`,
749 | // },
750 | // ],
751 | // } as const;
752 | // } catch (error) {
753 | // console.error('Error getting transaction history:', error);
754 | // return {
755 | // content: [
756 | // {
757 | // type: 'text',
758 | // text: `Error getting transaction history: ${formatError(error)}`,
759 | // },
760 | // ],
761 | // } as const;
762 | // }
763 | // },
764 |
765 | async upload_image(args: unknown) {
766 | try {
767 | const { imageURL } = schemas.toolInputs.uploadImage.parse(args);
768 |
769 | if (!server.kaiaFunClient) throw new Error('KaiaFun client not initialized');
770 |
771 | try {
772 | // Download the image from the URL and save to temp file
773 | const { filePath, fileName, mimeType } = await downloadImageToTempFile(imageURL);
774 |
775 | // Create a File object from the temp file
776 | const file = new File([fs.readFileSync(filePath)], fileName, { type: mimeType });
777 |
778 | // Upload the image using KaiaFun's uploadImage method
779 | const uploadedUrl = await server.kaiaFunClient.uploadImage(file);
780 |
781 | if (!uploadedUrl) {
782 | throw new Error('Failed to upload image to KaiaFun server');
783 | }
784 |
785 | // Clean up the temp file
786 | fs.unlinkSync(filePath);
787 |
788 | return {
789 | content: [
790 | {
791 | type: 'text',
792 | text: `Successfully uploaded image to KaiaFun!
793 | Original URL: ${imageURL}
794 | Uploaded URL: ${uploadedUrl}`,
795 | },
796 | ],
797 | } as const;
798 | } catch (error) {
799 | return {
800 | content: [
801 | {
802 | type: 'text',
803 | text: `Error processing image: ${formatError(error)}`,
804 | },
805 | ],
806 | } as const;
807 | }
808 | } catch (error) {
809 | console.error('Error uploading image:', error);
810 | return {
811 | content: [
812 | {
813 | type: 'text',
814 | text: `Error uploading image: ${formatError(error)}`,
815 | },
816 | ],
817 | } as const;
818 | }
819 | },
820 | });
821 |
822 | // Initialize MCP server
823 | const server: KaiaFunServer = new Server(
824 | {
825 | name: 'kaiafun-server',
826 | version: '1.0.0',
827 | },
828 | {
829 | capabilities: {
830 | tools: {},
831 | resources: {},
832 | },
833 | },
834 | );
835 |
836 | // Initialize image cache
837 | server.imageCache = new Map();
838 |
839 | // Create tool handlers with access to the server
840 | const toolHandlers = createToolHandlers(server);
841 |
842 | // Register tool handlers
843 | server.setRequestHandler(ListToolsRequestSchema, async () => {
844 | console.error('Tools requested by client');
845 | return { tools: TOOL_DEFINITIONS };
846 | });
847 |
848 | server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
849 | const { name, arguments: args } = request.params;
850 |
851 | try {
852 | const handler = toolHandlers[name as keyof typeof toolHandlers];
853 | if (!handler) {
854 | throw new Error(`Unknown tool: ${name}`);
855 | }
856 |
857 | return await handler(args);
858 | } catch (error) {
859 | console.error(`Error executing tool ${name}:`, error);
860 | throw error;
861 | }
862 | });
863 |
864 | // Register resource handlers
865 | server.setRequestHandler(ListResourcesRequestSchema, async () => {
866 | if (!server.imageCache) {
867 | return { resources: [] };
868 | }
869 |
870 | const resources = Array.from(server.imageCache.entries()).map(([uri, info]) => ({
871 | uri,
872 | name: `Image: ${path.basename(uri)}`,
873 | description: 'Uploaded image resource',
874 | mimeType: info.mimeType,
875 | }));
876 |
877 | return { resources };
878 | });
879 |
880 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
881 | const uri = request.params.uri;
882 |
883 | if (!server.imageCache || !server.imageCache.has(uri)) {
884 | throw new Error(`Resource not found: ${uri}`);
885 | }
886 |
887 | const imageInfo = server.imageCache.get(uri);
888 | if (!imageInfo) {
889 | throw new Error(`Resource not found: ${uri}`);
890 | }
891 |
892 | try {
893 | // Read the image file
894 | const imageBuffer = fs.readFileSync(imageInfo.path);
895 |
896 | // Return the image as a binary resource
897 | return {
898 | contents: [
899 | {
900 | uri,
901 | mimeType: imageInfo.mimeType,
902 | blob: imageBuffer.toString('base64'),
903 | },
904 | ],
905 | };
906 | } catch (error) {
907 | console.error(`Error reading resource ${uri}:`, error);
908 | throw new Error(`Failed to read resource: ${formatError(error)}`);
909 | }
910 | });
911 |
912 | // Start the server
913 | async function main() {
914 | if (!process.env.PRIVATE_KEY) {
915 | throw new Error('PRIVATE_KEY environment variable is required');
916 | }
917 |
918 | // Initialize `KaiaFunClient` once when server starts and connect to server instance
919 | server.kaiaFunClient = setupKaiaFunClient(process.env.PRIVATE_KEY as Hex);
920 | console.error('KaiaFun client initialized');
921 |
922 | const transport = new StdioServerTransport();
923 | await server.connect(transport);
924 | console.error('KaiaFun MCP Server running on stdio');
925 | }
926 |
927 | main().catch((error) => {
928 | console.error('Fatal error:', error);
929 | process.exit(1);
930 | });
931 |
```