Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ If you want to add a new tool to the Base MCP server, follow these steps:

1. Create a new directory in the `src/tools` directory for your tool
2. Implement the tool following the existing patterns:
- `index.ts`: Define and export your tools
- `index.ts`: Define and export your tools. Tools are defined as AgentKit ActionProviders.
- `schemas.ts`: Define input schemas for your tools
- `handlers.ts`: Implement the functionality of your tools
3. Add your tool to the list of available tools in `src/tools/index.ts`
- `types.ts`: Define types required for your tools
- `utils.ts`: Utilities for your tools
3. Add your tool to the list of available tools in `src/main.ts`
4. Add documentation for your tool in the README.md
5. Add examples of how to use your tool in examples.md
6. Write tests for your tool
Expand All @@ -50,11 +51,9 @@ The Base MCP server follows this structure for tools:
```
src/
├── tools/
│ ├── index.ts (exports toolsets)
│ ├── [TOOL_NAME]/ <-------------------------- ADD DIR HERE
│ │ ├── index.ts (defines and exports tools)
│ │ ├── schemas.ts (defines input schema)
│ │ └── handlers.ts (implements tool functionality)
│ └── utils/ (shared tool utilities)
```

Expand Down
47 changes: 8 additions & 39 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,16 @@ import {
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import * as dotenv from 'dotenv';
import {
createWalletClient,
http,
publicActions,
type PublicActions,
type WalletClient,
} from 'viem';
import { english, generateMnemonic, mnemonicToAccount } from 'viem/accounts';
import { english, generateMnemonic } 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 { baseMcpErc20ActionProvider } from './tools/erc20/index.js';
import { baseMcpMorphoActionProvider } from './tools/morpho/index.js';
import { baseMcpNftActionProvider } from './tools/nft/index.js';
import { baseMcpOnrampActionProvider } from './tools/onramp/index.js';
import { openRouterActionProvider } from './tools/open-router/index.js';
import {
generateSessionId,
getActionProvidersWithRequiredEnvVars,
Expand Down Expand Up @@ -65,12 +60,6 @@ export async function main() {
);
}

const viemClient = createWalletClient({
account: mnemonicToAccount(seedPhrase ?? fallbackPhrase),
chain,
transport: http(),
}).extend(publicActions) as WalletClient & PublicActions;

const cdpWalletProvider = await CdpWalletProvider.configureWithWallet({
mnemonicPhrase: seedPhrase ?? fallbackPhrase,
apiKeyName,
Expand Down Expand Up @@ -100,6 +89,9 @@ export async function main() {
baseMcpMorphoActionProvider(),
baseMcpContractActionProvider(),
baseMcpOnrampActionProvider(),
baseMcpErc20ActionProvider(),
baseMcpNftActionProvider(),
openRouterActionProvider(),
],
});

Expand Down Expand Up @@ -128,37 +120,14 @@ export async function main() {
console.error('Received ListToolsRequest');

return {
tools: [...baseMcpTools, ...tools],
tools,
};
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
postMetric(Event.ToolUsed, { toolName: request.params.name }, sessionId);

// Check if the tool is Base MCP tool
const isBaseMcpTool = baseMcpTools.some(
(tool) => tool.definition.name === request.params.name,
);

if (isBaseMcpTool) {
const tool = toolToHandler[request.params.name];
if (!tool) {
throw new Error(`Tool ${request.params.name} not found`);
}

const result = await tool(viemClient, request.params.arguments);

return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
};
}

// In order for users to use AgentKit tools, they are required to have a SEED_PHRASE and not a ONE_TIME_KEY
if (!seedPhrase) {
return {
Expand Down
79 changes: 0 additions & 79 deletions src/tools/erc20/handlers.ts

This file was deleted.

123 changes: 108 additions & 15 deletions src/tools/erc20/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,110 @@
import { generateTool } from '../../utils.js';
import { erc20BalanceHandler, erc20TransferHandler } from './handlers.js';
import {
ActionProvider,
CreateAction,
EvmWalletProvider,
type Network,
} from '@coinbase/agentkit';
import {
encodeFunctionData,
erc20Abi,
formatUnits,
isAddress,
parseUnits,
} 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 { Erc20BalanceSchema, Erc20TransferSchema } from './schemas.js';

export const erc20BalanceTool = generateTool({
name: 'erc20_balance',
description: 'Get the balance of an ERC20 token',
inputSchema: Erc20BalanceSchema,
toolHandler: erc20BalanceHandler,
});

export const erc20TransferTool = generateTool({
name: 'erc20_transfer',
description: 'Transfer an ERC20 token',
inputSchema: Erc20TransferSchema,
toolHandler: erc20TransferHandler,
});
export class BaseMcpErc20ActionProvider extends ActionProvider<EvmWalletProvider> {
constructor() {
super('baseMcpErc20', []);
}

@CreateAction({
name: 'erc20_balance',
description: 'Get the balance of an ERC20 token',
schema: Erc20BalanceSchema,
})
async erc20Balance(
walletProvider: EvmWalletProvider,
args: z.infer<typeof Erc20BalanceSchema>,
) {
const { contractAddress } = args;

if (!isAddress(contractAddress, { strict: false })) {
throw new Error(`Invalid contract address: ${contractAddress}`);
}

const balance = await walletProvider.readContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [(walletProvider.getAddress() as `0x${string}`) ?? '0x'],
});

const decimals = await walletProvider.readContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'decimals',
});

return formatUnits(balance, decimals);
}

@CreateAction({
name: 'erc20_transfer',
description: 'Transfer an ERC20 token',
schema: Erc20TransferSchema,
})
async erc20Transfer(
walletProvider: EvmWalletProvider,
args: z.infer<typeof Erc20TransferSchema>,
) {
const { contractAddress, toAddress, amount } = args;

if (!isAddress(contractAddress, { strict: false })) {
throw new Error(`Invalid contract address: ${contractAddress}`);
}

if (!isAddress(toAddress, { strict: false })) {
throw new Error(`Invalid to address: ${toAddress}`);
}

const decimals = await walletProvider.readContract({
address: contractAddress,
abi: erc20Abi,
functionName: 'decimals',
});

const atomicUnits = parseUnits(amount, decimals);

const tx = await walletProvider.sendTransaction({
to: contractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [toAddress, atomicUnits],
}),
});

const chain =
chainIdToChain(walletProvider.getNetwork().chainId ?? base.id) ?? base;

return JSON.stringify({
hash: tx,
url: constructBaseScanUrl(chain, tx),
});
}

supportsNetwork(network: Network): boolean {
return (
network.chainId === String(base.id) ||
network.chainId === String(baseSepolia.id)
);
}
}

export const baseMcpErc20ActionProvider = () =>
new BaseMcpErc20ActionProvider();
Loading