From 546c8a16d032116dd87d1557d3b3a079f0871d85 Mon Sep 17 00:00:00 2001 From: dschlabach Date: Tue, 15 Apr 2025 14:11:17 -0400 Subject: [PATCH 1/7] test --- package.json | 6 ++--- src/main.ts | 36 +++++++++++++++++++----------- src/tools/index.ts | 3 +-- src/tools/morpho/index.ts | 46 ++++++++++++++++++++++++++++++++------- yarn.lock | 24 +++++++++++++++----- 5 files changed, 83 insertions(+), 32 deletions(-) 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..67cd4ea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,6 +28,7 @@ import { base } from 'viem/chains'; import { Event, postMetric } from './analytics.js'; import { chainIdToCdpNetworkId, chainIdToChain } from './chains.js'; import { baseMcpTools, toolToHandler } from './tools/index.js'; +import { baseMcpMorphoActionProvider } from './tools/morpho/index.js'; import { generateSessionId, getActionProvidersWithRequiredEnvVars, @@ -80,22 +81,30 @@ export async function main() { cdpApiKeyPrivateKey: privateKey, walletProvider: cdpWalletProvider, actionProviders: [ - basenameActionProvider(), - morphoActionProvider(), - walletActionProvider(), - cdpWalletActionProvider({ - apiKeyName, - apiKeyPrivateKey: privateKey, - }), - cdpApiActionProvider({ - apiKeyName, - apiKeyPrivateKey: privateKey, - }), - ...getActionProvidersWithRequiredEnvVars(), + // basenameActionProvider(), + // morphoActionProvider(), + // walletActionProvider(), + // cdpWalletActionProvider({ + // apiKeyName, + // apiKeyPrivateKey: privateKey, + // }), + // cdpApiActionProvider({ + // apiKeyName, + // apiKeyPrivateKey: privateKey, + // }), + // ...getActionProvidersWithRequiredEnvVars(), + + // Base MCP Action Providers + baseMcpMorphoActionProvider(), ], }); + console.error(' baseMcpMorphoActionProvider:', baseMcpMorphoActionProvider); + + const actions = agentKit.getActions(); + console.error(' actions:', actions); const { tools, toolHandler } = await getMcpTools(agentKit); + console.error(' tools:', tools); const server = new Server( { @@ -119,7 +128,8 @@ export async function main() { server.setRequestHandler(ListToolsRequestSchema, async () => { console.error('Received ListToolsRequest'); return { - tools: [...baseMcpTools.map((tool) => tool.definition), ...tools], + // tools: [...baseMcpTools.map((tool) => tool.definition), ...tools], + tools, }; }); diff --git a/src/tools/index.ts b/src/tools/index.ts index 51bc97e..72ecfc5 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,13 +1,12 @@ import { callContractTool } from './contracts/index.js'; import { erc20BalanceTool, erc20TransferTool } from './erc20/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, + // getMorphoVaultsTool, - using action provider to test callContractTool, getOnrampAssetsTool, onrampTool, diff --git a/src/tools/morpho/index.ts b/src/tools/morpho/index.ts index bab524b..48bd218 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, + type 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 e8b2848..34077b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -393,14 +393,15 @@ __metadata: languageName: node linkType: hard -"@coinbase/agentkit@npm:^0.6.0": - version: 0.6.0 - resolution: "@coinbase/agentkit@npm:0.6.0" +"@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" "@coinbase/coinbase-sdk": "npm:^0.20.0" "@jup-ag/api": "npm:^6.0.39" + "@privy-io/public-api": "npm:^2.18.5" "@privy-io/server-auth": "npm:^1.18.4" "@solana/spl-token": "npm:^0.4.12" "@solana/web3.js": "npm:^1.98.0" @@ -414,7 +415,7 @@ __metadata: twitter-api-v2: "npm:^1.18.2" viem: "npm:^2.22.16" zod: "npm:^3.23.8" - checksum: 10c0/a530ab86195b984cdd413a44e4233acdb20de7a7add15eabeb27cef9b5eb92788ab347a02d48d73ca5fa4b0c416dd2ca7f41bf2fb8c13ef1fb4f22ecd4af52cf + checksum: 10c0/2ccc8bca3748926757c24b21279ebeb01833499d8271b114684670bd38ee842c190beb0cea4f4bea19e0c90ae19d483bc9c76591fe484b2995a85fd365ac4221 languageName: node linkType: hard @@ -1728,6 +1729,19 @@ __metadata: languageName: node linkType: hard +"@privy-io/public-api@npm:^2.18.5": + version: 2.21.2 + resolution: "@privy-io/public-api@npm:2.21.2" + dependencies: + "@privy-io/api-base": "npm:1.4.5" + bs58: "npm:^5.0.0" + libphonenumber-js: "npm:^1.10.31" + viem: "npm:^2" + zod: "npm:^3.22.4" + checksum: 10c0/125d6e73d55ec7b8aa666f36a539adb0f6091d63de1719cb6d947f1929764db70bdf7d6d37be631bc13268bb92f52c6a8d2ce62cfceb305acdee4c0cd33519de + languageName: node + linkType: hard + "@privy-io/public-api@npm:^2.20.5": version: 2.20.5 resolution: "@privy-io/public-api@npm:2.20.5" @@ -3090,7 +3104,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" From fc666c3891abd8805e6d3cbb1457f86eb7c2b03c Mon Sep 17 00:00:00 2001 From: dschlabach Date: Wed, 16 Apr 2025 10:06:15 -0400 Subject: [PATCH 2/7] save --- src/main.ts | 9 ++------- src/tools/morpho/index.ts | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 67cd4ea..ac3aac8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -54,7 +54,7 @@ export async function main() { const sessionId = generateSessionId(); - postMetric(Event.Initialized, {}, sessionId); + // postMetric(Event.Initialized, {}, sessionId); const chain = chainIdToChain(chainId); if (!chain) { @@ -98,13 +98,8 @@ export async function main() { baseMcpMorphoActionProvider(), ], }); - console.error(' baseMcpMorphoActionProvider:', baseMcpMorphoActionProvider); - - const actions = agentKit.getActions(); - console.error(' actions:', actions); const { tools, toolHandler } = await getMcpTools(agentKit); - console.error(' tools:', tools); const server = new Server( { @@ -135,7 +130,7 @@ export async function main() { server.setRequestHandler(CallToolRequestSchema, async (request) => { try { - postMetric(Event.ToolUsed, { toolName: request.params.name }, sessionId); + // postMetric(Event.ToolUsed, { toolName: request.params.name }, sessionId); // Check if the tool is Base MCP tool const isBaseMcpTool = baseMcpTools.some( diff --git a/src/tools/morpho/index.ts b/src/tools/morpho/index.ts index 48bd218..fa9a0f5 100644 --- a/src/tools/morpho/index.ts +++ b/src/tools/morpho/index.ts @@ -23,6 +23,7 @@ export class BaseMcpMorphoActionProvider extends ActionProvider, ) { + console.error(' walletProvider:', walletProvider); const vaults = await getMorphoVaults({ chainId: Number(walletProvider.getNetwork().chainId), assetSymbol: args.assetSymbol ?? '', From bfac9ec18f11285ba78c3b0de94d777756e29b14 Mon Sep 17 00:00:00 2001 From: dschlabach Date: Wed, 16 Apr 2025 10:07:06 -0400 Subject: [PATCH 3/7] comment --- src/tools/morpho/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/morpho/index.ts b/src/tools/morpho/index.ts index fa9a0f5..1439a19 100644 --- a/src/tools/morpho/index.ts +++ b/src/tools/morpho/index.ts @@ -23,6 +23,9 @@ export class BaseMcpMorphoActionProvider extends ActionProvider, ) { + /** + * walletProvider isn't being passed. When it's logged, it's the value that `args` should be, { assetSymbol: 'ETH' } + */ console.error(' walletProvider:', walletProvider); const vaults = await getMorphoVaults({ chainId: Number(walletProvider.getNetwork().chainId), From b1a4edb4368741f26c0df2c0ae3eab504e983765 Mon Sep 17 00:00:00 2001 From: dschlabach Date: Wed, 16 Apr 2025 14:07:03 -0400 Subject: [PATCH 4/7] fix --- src/tools/morpho/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tools/morpho/index.ts b/src/tools/morpho/index.ts index 1439a19..7e02346 100644 --- a/src/tools/morpho/index.ts +++ b/src/tools/morpho/index.ts @@ -1,7 +1,7 @@ import { ActionProvider, CreateAction, - type EvmWalletProvider, + EvmWalletProvider, type Network, } from '@coinbase/agentkit'; import { base } from 'viem/chains'; @@ -23,10 +23,6 @@ export class BaseMcpMorphoActionProvider extends ActionProvider, ) { - /** - * walletProvider isn't being passed. When it's logged, it's the value that `args` should be, { assetSymbol: 'ETH' } - */ - console.error(' walletProvider:', walletProvider); const vaults = await getMorphoVaults({ chainId: Number(walletProvider.getNetwork().chainId), assetSymbol: args.assetSymbol ?? '', From 2a6298dbb43e92eeaff5eb00d6eba5c9869a65df Mon Sep 17 00:00:00 2001 From: dschlabach Date: Tue, 22 Apr 2025 10:55:07 -0400 Subject: [PATCH 5/7] save --- src/main.ts | 32 +++++----- src/tools/contracts/handlers.ts | 61 ------------------ src/tools/contracts/index.ts | 107 +++++++++++++++++++++++++++++--- src/tools/index.ts | 4 +- 4 files changed, 117 insertions(+), 87 deletions(-) delete mode 100644 src/tools/contracts/handlers.ts diff --git a/src/main.ts b/src/main.ts index ac3aac8..7eb39e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,6 +27,7 @@ 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 { @@ -54,7 +55,7 @@ export async function main() { const sessionId = generateSessionId(); - // postMetric(Event.Initialized, {}, sessionId); + postMetric(Event.Initialized, {}, sessionId); const chain = chainIdToChain(chainId); if (!chain) { @@ -81,21 +82,22 @@ export async function main() { cdpApiKeyPrivateKey: privateKey, walletProvider: cdpWalletProvider, actionProviders: [ - // basenameActionProvider(), - // morphoActionProvider(), - // walletActionProvider(), - // cdpWalletActionProvider({ - // apiKeyName, - // apiKeyPrivateKey: privateKey, - // }), - // cdpApiActionProvider({ - // apiKeyName, - // apiKeyPrivateKey: privateKey, - // }), - // ...getActionProvidersWithRequiredEnvVars(), + basenameActionProvider(), + morphoActionProvider(), + walletActionProvider(), + cdpWalletActionProvider({ + apiKeyName, + apiKeyPrivateKey: privateKey, + }), + cdpApiActionProvider({ + apiKeyName, + apiKeyPrivateKey: privateKey, + }), + ...getActionProvidersWithRequiredEnvVars(), // Base MCP Action Providers baseMcpMorphoActionProvider(), + baseMcpContractActionProvider(), ], }); @@ -122,9 +124,9 @@ export async function main() { server.setRequestHandler(ListToolsRequestSchema, async () => { console.error('Received ListToolsRequest'); + return { - // tools: [...baseMcpTools.map((tool) => tool.definition), ...tools], - 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/index.ts b/src/tools/index.ts index 72ecfc5..144c14c 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,4 +1,4 @@ -import { callContractTool } from './contracts/index.js'; +// import { callContractTool } from './contracts/index.js'; import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js'; import { listNftsTool, transferNftTool } from './nft/index.js'; import { getOnrampAssetsTool, onrampTool } from './onramp/index.js'; @@ -6,8 +6,6 @@ import { buyOpenRouterCreditsTool } from './open-router/index.js'; import type { ToolHandler, ToolWithHandler } from './types.js'; export const baseMcpTools: ToolWithHandler[] = [ - // getMorphoVaultsTool, - using action provider to test - callContractTool, getOnrampAssetsTool, onrampTool, erc20BalanceTool, From 2e125ab63852a5e896148f82ace636088ee0b81f Mon Sep 17 00:00:00 2001 From: dschlabach Date: Tue, 22 Apr 2025 10:55:59 -0400 Subject: [PATCH 6/7] save --- src/tools/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index 144c14c..ae39b22 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,4 +1,3 @@ -// import { callContractTool } from './contracts/index.js'; import { erc20BalanceTool, erc20TransferTool } from './erc20/index.js'; import { listNftsTool, transferNftTool } from './nft/index.js'; import { getOnrampAssetsTool, onrampTool } from './onramp/index.js'; From cc73fb4b05affdd3a78fd35d272840bf46f7e947 Mon Sep 17 00:00:00 2001 From: dschlabach Date: Tue, 22 Apr 2025 10:58:27 -0400 Subject: [PATCH 7/7] fix --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 7eb39e5..8e3b168 100644 --- a/src/main.ts +++ b/src/main.ts @@ -132,7 +132,7 @@ export async function main() { server.setRequestHandler(CallToolRequestSchema, async (request) => { try { - // postMetric(Event.ToolUsed, { toolName: request.params.name }, sessionId); + postMetric(Event.ToolUsed, { toolName: request.params.name }, sessionId); // Check if the tool is Base MCP tool const isBaseMcpTool = baseMcpTools.some(