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
5 changes: 5 additions & 0 deletions typescript/.changeset/breezy-badgers-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-onchain-agent": minor
---

Added option to generate MCP Server project
2 changes: 1 addition & 1 deletion typescript/.changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@coinbase/*-example"],
"ignore": ["@coinbase/*-example", "mcp"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
Expand Down
3 changes: 2 additions & 1 deletion typescript/create-onchain-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
34 changes: 34 additions & 0 deletions typescript/create-onchain-agent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isValidPackageName,
toValidPackageName,
getWalletProviders,
handleMcpSelection,
} from "./utils.js";
import { Frameworks, FrameworkToTemplates } from "./constants.js";

Expand Down Expand Up @@ -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;
}
Expand Down
47 changes: 44 additions & 3 deletions typescript/create-onchain-agent/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AgentkitRouteConfiguration } from "./types";
import { AgentkitRouteConfiguration, MCPRouteConfiguration } from "./types";

export const EVM_NETWORKS = [
"base-mainnet",
Expand Down Expand Up @@ -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<Framework, Template[]> = {
Langchain: ["next"],
"Vercel AI SDK": ["next"],
"Model Context Protocol": ["mcp"],
};

export type NextTemplateRouteConfiguration = {
Expand All @@ -185,3 +186,43 @@ export const NextTemplateRouteConfigurations: Partial<
createAgentRoute: "vercel-ai-sdk/create-agent.ts",
},
};

export const MCPRouteConfigurations: Record<
"EVM" | "CUSTOM_EVM" | "SVM",
Partial<Record<WalletProviderChoice, MCPRouteConfiguration>>
> = {
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",
},
},
};
5 changes: 5 additions & 0 deletions typescript/create-onchain-agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
126 changes: 125 additions & 1 deletion typescript/create-onchain-agent/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
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<string, string> = Object.entries(
CHAIN_ID_TO_NETWORK_ID,
).reduce(
(acc, [chainId, networkId]) => {
acc[networkId] = String(chainId);
return acc;
},
{} as Record<string, string>,
);

/**
* Determines the network family based on the provided network.
*
Expand Down Expand Up @@ -192,7 +224,7 @@ export async function handleNextSelection(
network?: Network,
chainId?: string,
rpcUrl?: string,
) {
): Promise<void> {
const agentDir = path.join(root, "app", "api", "agent");

const networkFamily = getNetworkType(network, chainId);
Expand Down Expand Up @@ -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<void>} A promise that resolves when the function completes.
*/
export async function handleMcpSelection(
root: string,
walletProvider: WalletProviderChoice,
network?: Network,
chainId?: string,
rpcUrl?: string,
): Promise<void> {
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 });
}