Skip to main content
Glama

agentek-eth

by NaniDAO
CONTRIBUTING.md11.7 kB
# Contributing to Agentek Thank you for your interest in contributing to Agentek! This document provides guidelines and examples for adding new tools to the toolkit. ## Development Setup 1. Clone the repository and navigate to the project directory ```bash git clone https://github.com/NaniDAO/agentek.git cd agentek ``` 2. Install project dependencies using bun ```bash bun i ``` 3. Configure your environment variables ```bash cp .env.example .env ``` Open `.env` in your editor and add the required API keys You're now ready to start developing! You can: - Run the test suite: `bun test` - Try the examples - Start building your own tools or contribute to existing ones ## Tool Interface Each tool in Agentek follows a standard TypeScript interface enforced with the `createTool` function. Tools require: - name: A unique identifier string for the tool - description: A clear description of what the tool does - parameters: A Zod schema defining the expected arguments - execute: An async function that performs the tool's operation - supportedChains: (Optional) Array of viem blockchain network objects the tool supports ### Intent Tools Intent tools handle blockchain transactions and must follow this pattern: 1. Return an `Intent` object when no wallet is available 2. Execute the transaction when a wallet is available ```typescript interface Intent { intent: string; // Human readable description of the operation ops: Operation[]; // Array of operations to execute chain: number; // Chain ID hash?: string; // Transaction hash (only when executed) } interface Operation { target: Address; // Contract address to interact with value: string; // Amount of native token to send data: Hex; // Encoded function call data } ``` Example Intent Tool Pattern: ```typescript execute: async (client: AgentekClient, args) => { // ... preparation logic ... const ops: Operation[] = [ // Build your operations ]; const intentDescription = `Human readable description of what this does`; // If no wallet client, return unexecuted intent if (!walletClient) { return { intent: intentDescription, ops, chain: chainId, }; } // If wallet available, execute and return result with hash const hash = await client.executeOps(ops, chainId); return { intent: intentDescription, ops, chain: chainId, hash, }; } ``` ## Adding a New Tool 1. Create a new directory in `src/shared/` with your tool name (e.g., `myNewTool`) 2. Implement the Tool interface 3. Export your tool 4. Add it to the tools registry at `src/shared/index.ts` ### Example Tool Implementation #### Tools ```typescript export const scrapeWebContent = createTool({ name: "scrapeWebContent", description: "Given a URL, fetch the page's HTML and return the main text content as accurately as possible. Works for most websites.", parameters: z.object({ website: z.string(), }), execute: async (_client, args) => { const { website } = args; try { const response = await fetch(website); if (!response.ok) { throw new Error(`Failed to fetch URL (status: ${response.status}).`); } const html = await response.text(); const $ = cheerio.load(html); $("script, style, noscript").remove(); const textContent = $("body").text() || ""; const cleanedText = textContent.replace(/\s+/g, " ").trim(); return { website, text: cleanedText, }; } catch (error: any) { throw new Error(`Error fetching text: ${error.message}`); } }, }); ``` #### Tools requiring API keys ```typescript export function createAskPerplexitySearchTool( perplexityApiKey: string, ): BaseTool { return createTool({ name: "askPerplexitySearch", description: "Ask perplexity search", supportedChains: [], parameters: z.object({ searchString: z.string(), }), execute: async (client, args) => { const options = { method: "POST", headers: { Authorization: `Bearer ${perplexityApiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "sonar", messages: [ { role: "system", content: "Be precise and concise.", }, { role: "user", content: args.searchString, }, ], temperature: 0.2, top_p: 0.9, search_domain_filter: ["perplexity.ai"], return_images: false, return_related_questions: false, search_recency_filter: "month", top_k: 0, stream: false, presence_penalty: 0, frequency_penalty: 1, response_format: null, }), }; try { const response = await fetch( "https://api.perplexity.ai/chat/completions", options, ); const result = await response.json(); return result; } catch (err) { throw new Error(`Perplexity API Error: ${err}`); } }, }); } ``` #### Intent Tools ```typescript export const createMatchSwapTool = ({ zeroxApiKey, }: { zeroxApiKey: string; }): BaseTool => { return createTool({ name: "intent0xSwap", description: "Perform a token swap on multiple EVM networks via 0x aggregator (Matcha)", // Include all chains you wish to support supportedChains: matchaSwapChains, parameters: z.object({ chainId: z.number().describe("Chain ID (e.g. 1, 10, 42161, 8453)"), fromToken: z .string() .describe('Source token address, or "ETH" for native'), toToken: z.string().describe("Destination token address"), amount: z.number().describe("Amount of source token to swap"), }), execute: async (client: AgentekClient, args) => { const { chainId, fromToken, toToken, amount } = args; // Prepare addresses const sellToken = normalize(fromToken); const buyToken = normalize(toToken); // Retrieve the relevant wallet + public clients const walletClient = client.getWalletClient(chainId); const publicClient = client.getPublicClient(chainId); const swapIntentDescription = `Swap ${amount} of ${fromToken} for ${toToken} on chainId ${chainId}`; try { // Determine decimals const sellDecimals = sellToken === "ETH" ? 18 : ((await publicClient.readContract({ address: sellToken as Address, abi: erc20Abi, functionName: "decimals", })) as number) || 18; const sellAmount = parseUnits(`${amount}`, sellDecimals); const ops = []; const userAddress = await client.getAddress(); // Check user's balance const userBalance = sellToken === "ETH" ? await publicClient.getBalance({ address: userAddress as Address }) : ((await publicClient.readContract({ address: sellToken as Address, abi: erc20Abi, functionName: "balanceOf", args: [userAddress], })) as bigint); if (userBalance < sellAmount) { throw new Error( `Insufficient balance: You have ${userBalance.toString()} ${sellToken} but trying to sell ${sellAmount.toString()} ${sellToken}`, ); } // Check allowance if we're selling ERC20 if (sellToken !== "ETH") { const EXCHANGE_PROXY = "0xdef1c0ded9bec7f1a1670819833240f027b25eff"; const currentAllowance = (await publicClient.readContract({ address: sellToken as Address, abi: erc20Abi, functionName: "allowance", args: [userAddress, EXCHANGE_PROXY], })) as bigint; if (sellAmount > currentAllowance) { ops.push({ target: sellToken as Address, value: "0", data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [EXCHANGE_PROXY, maxUint256], }), }); } } // Fetch quote from 0x const zeroXEndpoint = get0xApiEndpoint(chainId); const params = new URLSearchParams({ sellToken, buyToken, sellAmount: sellAmount.toString(), takerAddress: userAddress, }); const quoteUrl = `${zeroXEndpoint}/swap/v1/quote?${params}`; const quoteResp = await fetch(quoteUrl, { headers: { "0x-api-key": zeroxApiKey }, }); if (!quoteResp.ok) { throw new Error( `Failed to get swap quote: ${quoteResp.status} ${quoteResp.statusText}`, ); } const quote = await quoteResp.json(); if (!quote || quote.code) { throw new Error( quote.message || "Failed to retrieve a valid swap quote", ); } // Build aggregator swap call ops.push({ target: quote.to as Address, value: sellToken === "ETH" ? (quote.value as string) : "0", data: quote.data as Hex, }); // If no wallet client, return an unexecuted intent if (!walletClient) { return { intent: swapIntentDescription, ops, chain: chainId, }; } // If walletClient is present, execute ops const hash = await client.executeOps(ops, chainId); return { intent: swapIntentDescription, ops, chain: chainId, hash, }; } catch (error) { throw new Error( `Matcha Swap Failed: ${error instanceof Error ? error.message : error}`, ); } }, }); }; ``` ### Adding Tool to Registry 1. Export your tools using the collection pattern in your tool directory: ```typescript // src/shared/myNewTool/index.ts import { BaseTool, createToolCollection } from "../client.js"; import { myNewTool } from "./tools.js"; export function myNewToolTools(): BaseTool[] { return createToolCollection([myNewTool]); } ``` 2. If your tool requires an API key, export a function that accepts config: ```typescript export function myNewToolTools({ apiKey }: { apiKey: string }): BaseTool[] { return createToolCollection([myNewTool(apiKey)]); } ``` 3. Import and add your tools to the registry in `src/shared/index.ts`: ```typescript import { myNewToolTools } from "./myNewTool.js"; const allTools = ({ existingApiKey, myNewToolApiKey, // Add your API key param if needed }) => { let tools = [ // ...existingTools ...myNewToolTools(), // For tools without API keys ]; if (myNewToolApiKey) { tools.push(...myNewToolTools({ apiKey: myNewToolApiKey })); } return tools; }; ``` 4. Update the tool count script at `scripts/tool-count.ts` to include your API key: ```typescript const tools = allTools({ ...existingApiKeys, myNewToolApiKey: process.env.MY_NEW_TOOL_API_KEY, // Add your API key }); ``` ## Code Style - Always use clear, descriptive and meaningful names. This library is intended to be consumed by AI agents, making this vital. - Not using any linter or formatter right now but will add that. Expect updates here. ## Testing - Write unit tests for your tool using Vitest and TypeScript. - More comprehensive testing will be added in the future. ## Security Guidelines - Never commit API keys - Validate all user inputs with Zod schema and further sanitization if required

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NaniDAO/agentek'

If you have feedback or need assistance with the MCP directory API, please join our Discord server