From acaceb9331dd4d909fe5b46a947562df5ed7577a Mon Sep 17 00:00:00 2001 From: Ryan Gilbert Date: Fri, 14 Mar 2025 11:47:04 -0400 Subject: [PATCH] feat(create-onchain-agent): add mcp to typescript cli --- typescript/.changeset/breezy-badgers-marry.md | 5 + typescript/.changeset/config.json | 2 +- typescript/create-onchain-agent/README.md | 3 +- typescript/create-onchain-agent/src/cli.ts | 34 +++++ .../create-onchain-agent/src/constants.ts | 47 ++++++- typescript/create-onchain-agent/src/types.ts | 5 + typescript/create-onchain-agent/src/utils.ts | 126 +++++++++++++++++- 7 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 typescript/.changeset/breezy-badgers-marry.md diff --git a/typescript/.changeset/breezy-badgers-marry.md b/typescript/.changeset/breezy-badgers-marry.md new file mode 100644 index 000000000..82a8ae017 --- /dev/null +++ b/typescript/.changeset/breezy-badgers-marry.md @@ -0,0 +1,5 @@ +--- +"create-onchain-agent": minor +--- + +Added option to generate MCP Server project diff --git a/typescript/.changeset/config.json b/typescript/.changeset/config.json index a6e6bb6a6..c1f1fa062 100644 --- a/typescript/.changeset/config.json +++ b/typescript/.changeset/config.json @@ -7,7 +7,7 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@coinbase/*-example"], + "ignore": ["@coinbase/*-example", "mcp"], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } diff --git a/typescript/create-onchain-agent/README.md b/typescript/create-onchain-agent/README.md index ec8d5781f..02153a3e6 100644 --- a/typescript/create-onchain-agent/README.md +++ b/typescript/create-onchain-agent/README.md @@ -27,7 +27,8 @@ This command will guide you through setting up an onchain agent project by promp - **Supports multiple AI frameworks**. - **Supports multiple blockchain networks**. - **Select your preferred wallet provider**. -- **Preconfigured with Next.js, React, Tailwind CSS, and ESLint**. +- **Supports a preconfigured Next.js project with React, Tailwind CSS, and ESLint**. +- **Supports a preconfigured Model Context Protocol Server project**. - **Automates environment setup**. ### **Setup Process** diff --git a/typescript/create-onchain-agent/src/cli.ts b/typescript/create-onchain-agent/src/cli.ts index 5abf9c591..8bf28f960 100644 --- a/typescript/create-onchain-agent/src/cli.ts +++ b/typescript/create-onchain-agent/src/cli.ts @@ -12,6 +12,7 @@ import { isValidPackageName, toValidPackageName, getWalletProviders, + handleMcpSelection, } from "./utils.js"; import { Frameworks, FrameworkToTemplates } from "./constants.js"; @@ -268,6 +269,39 @@ async function init() { console.log(" - mv .env.local .env"); console.log(" - npm run dev"); break; + case "mcp": + await handleMcpSelection(root, walletProvider, network, chainId); + + spinner.succeed(); + console.log(pc.blueBright(`\nSuccessfully created your AgentKit project in ${root}`)); + + console.log(`\nTo get started, run the following commands:\n`); + if (root !== process.cwd()) { + console.log(` - cd ${path.relative(process.cwd(), root)}`); + } + console.log(" - npm install"); + console.log(" - npm run build"); + console.log( + " - cp claude_desktop_config.json ~/Library/Application\\ Support/Claude/claude_desktop_config.json", + ); + + if (walletProvider === "CDP" || walletProvider === "SmartWallet") { + console.log( + " - # Make sure to open claude_desktop_config.json and configure your CDP API keys!", + ); + } + + if (walletProvider === "Privy") { + console.log( + " - # Make sure to open claude_desktop_config.json and configure your Privy API keys!", + ); + } + + console.log( + "\nNow open Claude Desktop and start prompting Claude to do things onchain", + "\nFor example, ask it to print your wallet details", + ); + break; default: break; } diff --git a/typescript/create-onchain-agent/src/constants.ts b/typescript/create-onchain-agent/src/constants.ts index c8bfc563a..16f12cd3a 100644 --- a/typescript/create-onchain-agent/src/constants.ts +++ b/typescript/create-onchain-agent/src/constants.ts @@ -1,4 +1,4 @@ -import { AgentkitRouteConfiguration } from "./types"; +import { AgentkitRouteConfiguration, MCPRouteConfiguration } from "./types"; export const EVM_NETWORKS = [ "base-mainnet", @@ -155,17 +155,18 @@ export const AgentkitRouteConfigurations: Record< }, }; -export const Frameworks = ["Langchain", "Vercel AI SDK"] as const; +export const Frameworks = ["Langchain", "Vercel AI SDK", "Model Context Protocol"] as const; export type Framework = (typeof Frameworks)[number]; -export const Templates = ["next"] as const; +export const Templates = ["next", "mcp"] as const; export type Template = (typeof Templates)[number]; export const FrameworkToTemplates: Record = { Langchain: ["next"], "Vercel AI SDK": ["next"], + "Model Context Protocol": ["mcp"], }; export type NextTemplateRouteConfiguration = { @@ -185,3 +186,43 @@ export const NextTemplateRouteConfigurations: Partial< createAgentRoute: "vercel-ai-sdk/create-agent.ts", }, }; + +export const MCPRouteConfigurations: Record< + "EVM" | "CUSTOM_EVM" | "SVM", + Partial> +> = { + EVM: { + CDP: { + getAgentkitRoute: "evm/cdp/getAgentKit.ts", + configRoute: "evm/cdp/claude_desktop_config.json", + }, + Viem: { + getAgentkitRoute: "evm/viem/getAgentKit.ts", + configRoute: "evm/viem/claude_desktop_config.json", + }, + Privy: { + getAgentkitRoute: "evm/privy/getAgentKit.ts", + configRoute: "evm/privy/claude_desktop_config.json", + }, + SmartWallet: { + getAgentkitRoute: "evm/smart/getAgentKit.ts", + configRoute: "evm/smart/claude_desktop_config.json", + }, + }, + CUSTOM_EVM: { + Viem: { + getAgentkitRoute: "custom-evm/viem/getAgentKit.ts", + configRoute: "custom-evm/viem/claude_desktop_config.json", + }, + }, + SVM: { + SolanaKeypair: { + getAgentkitRoute: "svm/solana-keypair/getAgentKit.ts", + configRoute: "svm/solana-keypair/claude_desktop_config.json", + }, + Privy: { + getAgentkitRoute: "svm/privy/getAgentKit.ts", + configRoute: "svm/privy/claude_desktop_config.json", + }, + }, +}; diff --git a/typescript/create-onchain-agent/src/types.ts b/typescript/create-onchain-agent/src/types.ts index c14ca347e..add17ac10 100644 --- a/typescript/create-onchain-agent/src/types.ts +++ b/typescript/create-onchain-agent/src/types.ts @@ -9,6 +9,11 @@ export type AgentkitRouteConfiguration = { prepareAgentkitRoute: `${string}.ts`; }; +export type MCPRouteConfiguration = { + getAgentkitRoute: `${string}.ts`; + configRoute: `${string}.json`; +}; + export type NetworkSelection = { networkFamily: "EVM" | "SVM"; networkType: "mainnet" | "testnet" | "custom"; diff --git a/typescript/create-onchain-agent/src/utils.ts b/typescript/create-onchain-agent/src/utils.ts index 9c10a1333..e326e2b8e 100644 --- a/typescript/create-onchain-agent/src/utils.ts +++ b/typescript/create-onchain-agent/src/utils.ts @@ -8,8 +8,40 @@ import { SVM_NETWORKS, AgentkitRouteConfigurations, NextTemplateRouteConfigurations, + MCPRouteConfigurations, } from "./constants.js"; +/** + * Copied from `@coinbase/agentkit` so that we don't need to depend on it. + * Maps EVM chain IDs to Coinbase network IDs + */ +export const CHAIN_ID_TO_NETWORK_ID: Record = { + 1: "ethereum-mainnet", + 11155111: "ethereum-sepolia", + 137: "polygon-mainnet", + 80001: "polygon-mumbai", + 8453: "base-mainnet", + 84532: "base-sepolia", + 42161: "arbitrum-mainnet", + 421614: "arbitrum-sepolia", + 10: "optimism-mainnet", + 11155420: "optimism-sepolia", +}; + +/** + * Copied from `@coinbase/agentkit` so that we don't need to depend on it. + * Maps Coinbase network IDs to EVM chain IDs + */ +export const NETWORK_ID_TO_CHAIN_ID: Record = Object.entries( + CHAIN_ID_TO_NETWORK_ID, +).reduce( + (acc, [chainId, networkId]) => { + acc[networkId] = String(chainId); + return acc; + }, + {} as Record, +); + /** * Determines the network family based on the provided network. * @@ -192,7 +224,7 @@ export async function handleNextSelection( network?: Network, chainId?: string, rpcUrl?: string, -) { +): Promise { const agentDir = path.join(root, "app", "api", "agent"); const networkFamily = getNetworkType(network, chainId); @@ -252,3 +284,95 @@ export async function handleNextSelection( await fs.rm(path.join(agentDir, "agentkit"), { recursive: true, force: true }); await fs.rm(path.join(agentDir, "framework"), { recursive: true, force: true }); } + +/** + * Handles the selection of a network and wallet provider, updating the project configuration accordingly. + * + * This function: + * - Determines the network family (`EVM` or `SVM`) based on the provided network or chain ID. + * - Retrieves the correct route configuration for the selected wallet provider. + * - Creates or updates the `.env.local` file with required and optional environment variables. + * - Moves the selected API route file to `api/agent/route.ts`. + * - Deletes all unselected API routes and cleans up empty directories. + * + * @param {string} root - The root directory of the project. + * @param {WalletProviderChoice} walletProvider - The selected wallet provider. + * @param {Network} [network] - The optional blockchain network. + * @param {string} [chainId] - The optional chain ID for the network. + * @param {string} [rpcUrl] - The optional RPC URL for the network. + * + * @returns {Promise} A promise that resolves when the function completes. + */ +export async function handleMcpSelection( + root: string, + walletProvider: WalletProviderChoice, + network?: Network, + chainId?: string, + rpcUrl?: string, +): Promise { + const srcDir = path.join(root, "src"); + + const networkFamily = getNetworkType(network, chainId); + if (!networkFamily) { + throw new Error("Unsupported network and chainId selected"); + } + + const mcpConfig = MCPRouteConfigurations[networkFamily][walletProvider]; + if (!mcpConfig) { + throw new Error("Selected invalid network & wallet provider combination"); + } + + /** + * Copies the claude_desktop_config.json file to the root directory + * and replaces the {parentFolderPath} placeholder with the absolute path. + */ + async function copyAndReplaceConfig() { + const existingConfig = await fs.readFile( + path.join(srcDir, "agentkit", mcpConfig!.configRoute), + "utf-8", + ); + const configJson = JSON.parse(existingConfig); + + configJson.mcpServers.agentkit.args[0] = configJson.mcpServers.agentkit.args[0].replace( + "{absolutePath}", + root, + ); + + if (network) { + // privy uses CHAIN_ID, others use NETWORK_ID + if (configJson.mcpServers.agentkit.env.NETWORK_ID) { + configJson.mcpServers.agentkit.env.NETWORK_ID = network; + } + + if (configJson.mcpServers.agentkit.env.CHAIN_ID) { + configJson.mcpServers.agentkit.env.CHAIN_ID = NETWORK_ID_TO_CHAIN_ID[network]; + } + } + + if (chainId) { + configJson.mcpServers.agentkit.env.CHAIN_ID = chainId; + } + + if (rpcUrl) { + configJson.mcpServers.agentkit.env.RPC_URL = rpcUrl; + } + + await fs.writeFile( + path.join(root, "claude_desktop_config.json"), + JSON.stringify(configJson, null, 2), + ); + } + + // Promote selected routes to + const promoteRoute = async (toPromote: string, to: string) => { + const selectedRoutePath = path.join(srcDir, "agentkit", toPromote); + const newRoutePath = path.join(srcDir, to); + await fs.rename(selectedRoutePath, newRoutePath); + }; + + await copyAndReplaceConfig(); + + await promoteRoute(mcpConfig.getAgentkitRoute, "getAgentKit.ts"); + + await fs.rm(path.join(srcDir, "agentkit"), { recursive: true, force: true }); +}