diff --git a/package.json b/package.json index cb3df63..0f628bc 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,7 @@ "name": "base-mcp", "version": "1.0.11", "description": "A Model Context Protocol (MCP) server that provides onchain tools for Claude AI, allowing it to interact with the Base blockchain and Coinbase API", - "bin": { - "base-mcp": "build/index.js" - }, + "bin": "build/index.js", "type": "module", "scripts": { "run": "tsx src/index.ts", @@ -43,7 +41,7 @@ }, "dependencies": { "@clack/prompts": "^0.10.0", - "@coinbase/agentkit": "^0.6.0", + "@coinbase/agentkit": "^0.6.2", "@coinbase/agentkit-model-context-protocol": "^0.2.0", "@coinbase/coinbase-sdk": "^0.21.0", "@coinbase/onchainkit": "^0.37.6", diff --git a/src/main.ts b/src/main.ts index d392261..8e3b168 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,7 +27,9 @@ import { english, generateMnemonic, mnemonicToAccount } from 'viem/accounts'; import { base } from 'viem/chains'; import { Event, postMetric } from './analytics.js'; import { chainIdToCdpNetworkId, chainIdToChain } from './chains.js'; +import { baseMcpContractActionProvider } from './tools/contracts/index.js'; import { baseMcpTools, toolToHandler } from './tools/index.js'; +import { baseMcpMorphoActionProvider } from './tools/morpho/index.js'; import { generateSessionId, getActionProvidersWithRequiredEnvVars, @@ -92,6 +94,10 @@ export async function main() { apiKeyPrivateKey: privateKey, }), ...getActionProvidersWithRequiredEnvVars(), + + // Base MCP Action Providers + baseMcpMorphoActionProvider(), + baseMcpContractActionProvider(), ], }); @@ -118,8 +124,9 @@ export async function main() { server.setRequestHandler(ListToolsRequestSchema, async () => { console.error('Received ListToolsRequest'); + return { - tools: [...baseMcpTools.map((tool) => tool.definition), ...tools], + tools: [...baseMcpTools, ...tools], }; }); diff --git a/src/tools/contracts/handlers.ts b/src/tools/contracts/handlers.ts deleted file mode 100644 index f507c09..0000000 --- a/src/tools/contracts/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Abi, AbiFunction, PublicActions, WalletClient } from 'viem'; -import { isAddress } from 'viem'; -import { base } from 'viem/chains'; -import type { z } from 'zod'; -import { constructBaseScanUrl } from '../utils/index.js'; -import type { CallContractSchema } from './schemas.js'; - -export async function callContractHandler( - wallet: WalletClient & PublicActions, - args: z.infer, -): Promise { - let abi: string | Abi = args.abi; - try { - abi = JSON.parse(abi) as Abi; - } catch (error) { - throw new Error(`Invalid ABI: ${error}`); - } - - if (!isAddress(args.contractAddress, { strict: false })) { - throw new Error(`Invalid contract address: ${args.contractAddress}`); - } - let functionAbi: AbiFunction | undefined; - - try { - functionAbi = abi.find( - (item) => 'name' in item && item.name === args.functionName, - ) as AbiFunction; - } catch (error) { - throw new Error(`Invalid function name: ${args.functionName}. ${error}`); - } - - if ( - functionAbi.stateMutability === 'view' || - functionAbi.stateMutability === 'pure' - ) { - const tx = await wallet.readContract({ - address: args.contractAddress, - abi, - functionName: args.functionName, - args: args.functionArgs, - }); - - return String(tx); - } - - const tx = await wallet.simulateContract({ - account: wallet.account, - abi, - address: args.contractAddress, - functionName: args.functionName, - value: BigInt(args.value ?? 0), - args: args.functionArgs, - }); - - const txHash = await wallet.writeContract(tx.request); - - return JSON.stringify({ - hash: txHash, - url: constructBaseScanUrl(wallet.chain ?? base, txHash), - }); -} diff --git a/src/tools/contracts/index.ts b/src/tools/contracts/index.ts index 3d11799..9b5f3c6 100644 --- a/src/tools/contracts/index.ts +++ b/src/tools/contracts/index.ts @@ -1,10 +1,101 @@ -import { generateTool } from '../../utils.js'; -import { callContractHandler } from './handlers.js'; +import { + ActionProvider, + CreateAction, + EvmWalletProvider, + type Network, +} from '@coinbase/agentkit'; +import { + encodeFunctionData, + isAddress, + type Abi, + type AbiFunction, +} from 'viem'; +import { base, baseSepolia } from 'viem/chains'; +import type { z } from 'zod'; +import { chainIdToChain } from '../../chains.js'; +import { constructBaseScanUrl } from '../utils/index.js'; import { CallContractSchema } from './schemas.js'; -export const callContractTool = generateTool({ - name: 'call_contract', - description: 'Call a contract function', - inputSchema: CallContractSchema, - toolHandler: callContractHandler, -}); +export class BaseMcpContractActionProvider extends ActionProvider { + constructor() { + super('baseMcpContract', []); + } + + @CreateAction({ + name: 'call_contract', + description: 'Call a contract function', + schema: CallContractSchema, + }) + async callContract( + walletProvider: EvmWalletProvider, + args: z.infer, + ) { + let abi: string | Abi = args.abi; + try { + abi = JSON.parse(abi) as Abi; + } catch (error) { + throw new Error(`Invalid ABI: ${error}`); + } + + if (!isAddress(args.contractAddress, { strict: false })) { + throw new Error(`Invalid contract address: ${args.contractAddress}`); + } + let functionAbi: AbiFunction | undefined; + + try { + functionAbi = abi.find( + (item) => 'name' in item && item.name === args.functionName, + ) as AbiFunction; + } catch (error) { + throw new Error(`Invalid function name: ${args.functionName}. ${error}`); + } + + const chain = chainIdToChain(Number(walletProvider.getNetwork().chainId)); + if (!chain) { + throw new Error( + `Unsupported chainId: ${walletProvider.getNetwork().chainId}`, + ); + } + + if ( + functionAbi.stateMutability === 'view' || + functionAbi.stateMutability === 'pure' + ) { + const tx = await walletProvider.readContract({ + address: args.contractAddress, + abi, + functionName: args.functionName, + args: args.functionArgs, + }); + + return String(tx); + } + + const tx = await walletProvider.sendTransaction({ + to: args.contractAddress, + data: encodeFunctionData({ + abi, + functionName: args.functionName, + args: args.functionArgs, + }), + value: BigInt(args.value ?? 0), + }); + + const link = constructBaseScanUrl(chain, tx); + + return JSON.stringify({ + hash: tx, + url: link, + }); + } + + supportsNetwork(network: Network): boolean { + return ( + network.chainId === String(base.id) || + network.chainId === String(baseSepolia.id) + ); + } +} + +export const baseMcpContractActionProvider = () => + new BaseMcpContractActionProvider(); diff --git a/src/tools/farcaster/handlers.ts b/src/tools/farcaster/handlers.ts index 41fb3cc..210f30c 100644 --- a/src/tools/farcaster/handlers.ts +++ b/src/tools/farcaster/handlers.ts @@ -4,92 +4,99 @@ import { FarcasterUsernameSchema } from './schemas.js'; // Neynar API response types type NeynarUserResponse = { - result: { - users: Array<{ - fid: number; - username: string; - verified_addresses: { - eth_addresses: string[]; - primary: { - eth_address: string; - }; - }; - }>; - }; + result: { + users: Array<{ + fid: number; + username: string; + verified_addresses: { + eth_addresses: string[]; + primary: { + eth_address: string; + }; + }; + }>; + }; }; export async function farcasterUsernameHandler( - wallet: WalletClient & PublicActions, - args: z.infer, + wallet: WalletClient & PublicActions, + args: z.infer, ): Promise { - const { username } = args; + const { username } = args; - // Get Neynar API key from environment variables - const neynarApiKey = process.env.NEYNAR_API_KEY; + // Get Neynar API key from environment variables + const neynarApiKey = process.env.NEYNAR_API_KEY; - if (!neynarApiKey) { - throw new Error('NEYNAR_API_KEY environment variable is not set'); - } + if (!neynarApiKey) { + throw new Error('NEYNAR_API_KEY environment variable is not set'); + } - try { - // Call Neynar API to search for the user - const response = await fetch( - `https://api.neynar.com/v2/farcaster/user/search?q=${encodeURIComponent(username)}`, - { - headers: { - 'accept': 'application/json', - 'x-api-key': neynarApiKey, - }, - } - ); + try { + // Call Neynar API to search for the user + const response = await fetch( + `https://api.neynar.com/v2/farcaster/user/search?q=${encodeURIComponent(username)}`, + { + headers: { + accept: 'application/json', + 'x-api-key': neynarApiKey, + }, + }, + ); - if (!response.ok) { - throw new Error(`Neynar API error: ${response.status} ${response.statusText}`); - } + if (!response.ok) { + throw new Error( + `Neynar API error: ${response.status} ${response.statusText}`, + ); + } - const data = (await response.json()) as NeynarUserResponse; + const data = (await response.json()) as NeynarUserResponse; - // Check if any users were found - if (!data.result.users || data.result.users.length === 0) { - return JSON.stringify({ - success: false, - message: `No Farcaster user found with username: ${username}`, - }); - } + // Check if any users were found + if (!data.result.users || data.result.users.length === 0) { + return JSON.stringify({ + success: false, + message: `No Farcaster user found with username: ${username}`, + }); + } - // Find the user with the matching username (case-insensitive) - const user = data.result.users.find( - (u) => u.username.toLowerCase() === username.toLowerCase() - ); + // Find the user with the matching username (case-insensitive) + const user = data.result.users.find( + (u) => u.username.toLowerCase() === username.toLowerCase(), + ); - if (!user) { - return JSON.stringify({ - success: false, - message: `No Farcaster user found with username: ${username}`, - }); - } + if (!user) { + return JSON.stringify({ + success: false, + message: `No Farcaster user found with username: ${username}`, + }); + } - // Check if the user has any verified Ethereum addresses - if (!user.verified_addresses?.eth_addresses || user.verified_addresses.eth_addresses.length === 0) { - return JSON.stringify({ - success: false, - message: `User ${username} has no verified Ethereum addresses`, - }); - } + // Check if the user has any verified Ethereum addresses + if ( + !user.verified_addresses?.eth_addresses || + user.verified_addresses.eth_addresses.length === 0 + ) { + return JSON.stringify({ + success: false, + message: `User ${username} has no verified Ethereum addresses`, + }); + } - // Return the primary Ethereum address if available, otherwise the first one - const ethAddress = user.verified_addresses.primary?.eth_address || user.verified_addresses.eth_addresses[0]; + // Return the primary Ethereum address if available, otherwise the first one + const ethAddress = + user.verified_addresses.primary?.eth_address || + user.verified_addresses.eth_addresses[0]; - return JSON.stringify({ - success: true, - username: user.username, - fid: user.fid, - ethAddress, - }); - } catch (error) { - return JSON.stringify({ - success: false, - message: `Error resolving Farcaster username: ${error instanceof Error ? error.message : String(error)}`, - }); - } -} \ No newline at end of file + return JSON.stringify({ + success: true, + username: user.username, + fid: user.fid, + ethAddress, + }); + } catch (error) { + return JSON.stringify({ + success: false, + message: `Error resolving Farcaster username: ${error instanceof Error ? error.message : String(error)}`, + }); + } +} diff --git a/src/tools/farcaster/index.ts b/src/tools/farcaster/index.ts index 4869e71..e1d9646 100644 --- a/src/tools/farcaster/index.ts +++ b/src/tools/farcaster/index.ts @@ -3,8 +3,8 @@ import { farcasterUsernameHandler } from './handlers.js'; import { FarcasterUsernameSchema } from './schemas.js'; export const farcasterUsernameTool = generateTool({ - name: 'farcaster_username', - description: 'Resolve a Farcaster username to an Ethereum address', - inputSchema: FarcasterUsernameSchema, - toolHandler: farcasterUsernameHandler, -}); \ No newline at end of file + name: 'farcaster_username', + description: 'Resolve a Farcaster username to an Ethereum address', + inputSchema: FarcasterUsernameSchema, + toolHandler: farcasterUsernameHandler, +}); diff --git a/src/tools/farcaster/schemas.ts b/src/tools/farcaster/schemas.ts index 05923fa..74de5e2 100644 --- a/src/tools/farcaster/schemas.ts +++ b/src/tools/farcaster/schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const FarcasterUsernameSchema = z.object({ - username: z - .string() - .describe('The Farcaster username to resolve to an Ethereum address'), -}); \ No newline at end of file + username: z + .string() + .describe('The Farcaster username to resolve to an Ethereum address'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 3f6b6e2..8cf89ba 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,15 +1,11 @@ -import { callContractTool } from './contracts/index.js'; import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js'; import { farcasterUsernameTool } from './farcaster/index.js'; -import { getMorphoVaultsTool } from './morpho/index.js'; import { listNftsTool, transferNftTool } from './nft/index.js'; import { getOnrampAssetsTool, onrampTool } from './onramp/index.js'; import { buyOpenRouterCreditsTool } from './open-router/index.js'; import type { ToolHandler, ToolWithHandler } from './types.js'; export const baseMcpTools: ToolWithHandler[] = [ - getMorphoVaultsTool, - callContractTool, getOnrampAssetsTool, onrampTool, erc20BalanceTool, diff --git a/src/tools/morpho/index.ts b/src/tools/morpho/index.ts index bab524b..7e02346 100644 --- a/src/tools/morpho/index.ts +++ b/src/tools/morpho/index.ts @@ -1,10 +1,40 @@ -import { generateTool } from '../../utils.js'; -import { getMorphoVaultsHandler } from './handlers.js'; +import { + ActionProvider, + CreateAction, + EvmWalletProvider, + type Network, +} from '@coinbase/agentkit'; +import { base } from 'viem/chains'; +import type { z } from 'zod'; import { GetMorphoVaultsSchema } from './schemas.js'; +import { getMorphoVaults } from './utils.js'; -export const getMorphoVaultsTool = generateTool({ - name: 'get_morpho_vaults', - description: 'Get the vaults available for a particular asset on Morpho', - inputSchema: GetMorphoVaultsSchema, - toolHandler: getMorphoVaultsHandler, -}); +export class BaseMcpMorphoActionProvider extends ActionProvider { + constructor() { + super('baseMcpMorpho', []); + } + + @CreateAction({ + name: 'get_morpho_vaults', + description: 'Get the vaults available for a particular asset on Morpho', + schema: GetMorphoVaultsSchema, + }) + async getMorphoVaults( + walletProvider: EvmWalletProvider, + args: z.infer, + ) { + const vaults = await getMorphoVaults({ + chainId: Number(walletProvider.getNetwork().chainId), + assetSymbol: args.assetSymbol ?? '', + }); + + return JSON.stringify(vaults); + } + + supportsNetwork(network: Network): boolean { + return network.chainId === String(base.id); + } +} + +export const baseMcpMorphoActionProvider = () => + new BaseMcpMorphoActionProvider(); diff --git a/yarn.lock b/yarn.lock index be62c69..182b43d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -393,9 +393,9 @@ __metadata: languageName: node linkType: hard -"@coinbase/agentkit@npm:^0.6.0": - version: 0.6.1 - resolution: "@coinbase/agentkit@npm:0.6.1" +"@coinbase/agentkit@npm:^0.6.2": + version: 0.6.2 + resolution: "@coinbase/agentkit@npm:0.6.2" dependencies: "@across-protocol/app-sdk": "npm:^0.2.0" "@alloralabs/allora-sdk": "npm:^0.1.0" @@ -415,7 +415,7 @@ __metadata: twitter-api-v2: "npm:^1.18.2" viem: "npm:^2.22.16" zod: "npm:^3.23.8" - checksum: 10c0/6f9241616dec0f62b4c1feb09cbc81e635bd6f09b1c12e2ba39c657f7e26936105f75b667036845cbe3a8cd5b0459d4eaa595a91b5d075f1e172dd45f5dbe3e3 + checksum: 10c0/2ccc8bca3748926757c24b21279ebeb01833499d8271b114684670bd38ee842c190beb0cea4f4bea19e0c90ae19d483bc9c76591fe484b2995a85fd365ac4221 languageName: node linkType: hard @@ -2764,9 +2764,9 @@ __metadata: linkType: hard "abbrev@npm:^3.0.0": - version: 3.0.1 - resolution: "abbrev@npm:3.0.1" - checksum: 10c0/21ba8f574ea57a3106d6d35623f2c4a9111d9ee3e9a5be47baed46ec2457d2eac46e07a5c4a60186f88cb98abbe3e24f2d4cca70bc2b12f1692523e2209a9ccf + version: 3.0.0 + resolution: "abbrev@npm:3.0.0" + checksum: 10c0/049704186396f571650eb7b22ed3627b77a5aedf98bb83caf2eac81ca2a3e25e795394b0464cfb2d6076df3db6a5312139eac5b6a126ca296ac53c5008069c28 languageName: node linkType: hard @@ -3091,7 +3091,7 @@ __metadata: dependencies: "@changesets/cli": "npm:^2.28.1" "@clack/prompts": "npm:^0.10.0" - "@coinbase/agentkit": "npm:^0.6.0" + "@coinbase/agentkit": "npm:^0.6.2" "@coinbase/agentkit-model-context-protocol": "npm:^0.2.0" "@coinbase/coinbase-sdk": "npm:^0.21.0" "@coinbase/onchainkit": "npm:^0.37.6"