UNPKG

story-build

Version:

MCP library for AI-assisted IP registration and licensing on Story Protocol

1 lines 253 kB
{"version":3,"sources":["../src/config.ts","../src/index.ts","../src/mcp/wallet/get_wallet_info_tool.ts","../src/mcp/wallet/get_account_balances_tool.ts","../src/mcp/wallet/send_eth_tool.ts","../src/mcp/wallet/send_token_tool.ts","../src/mcp/wallet/approve_token_tool.ts","../src/mcp/wallet/check_allowance_tool.ts","../src/mcp/wallet/get_token_info_tool.ts","../src/mcp/wallet/get_transaction_history_tool.ts","../src/mcp/wallet/validate_address_tool.ts","../src/mcp/wallet/wrap_ip_tool.ts","../src/mcp/wallet/unwrap_wip_tool.ts","../src/mcp/ip/register_ip_tool.ts","../src/utils/pinata.ts","../src/utils/url-parser.ts","../src/utils/license-terms.ts","../src/mcp/ip/get_ip_info_tool.ts","../src/mcp/license/create_license_terms_tool.ts","../src/mcp/license/attach_license_tool.ts","../src/mcp/license/mint_license_tool.ts","../src/mcp/webapp/generate_showcase_platform_tool.ts","../src/mcp/index.ts","../src/agent/index.ts"],"sourcesContent":["import { aeneid, mainnet, StoryClient, StoryConfig } from '@story-protocol/core-sdk';\nimport { Chain, createPublicClient, createWalletClient, http, WalletClient } from 'viem';\nimport { privateKeyToAccount, Address, Account } from 'viem/accounts';\nimport dotenv from 'dotenv';\n\ndotenv.config();\n\n// Network configuration types\ntype NetworkType = 'aeneid' | 'mainnet';\n\ninterface NetworkConfig {\n rpcProviderUrl: string;\n blockExplorer: string;\n protocolExplorer: string;\n defaultNFTContractAddress: Address | null;\n defaultSPGNFTContractAddress: Address | null;\n chain: Chain;\n}\n\nconst getArgs = () =>\n process.argv.reduce((args: any, arg: any) => {\n // long arg\n if (arg.slice(0, 2) === \"--\") {\n const longArg = arg.split(\"=\");\n const longArgFlag = longArg[0].slice(2);\n const longArgValue = longArg.length > 1 ? longArg[1] : true;\n args[longArgFlag] = longArgValue;\n }\n // flags\n else if (arg[0] === \"-\") {\n const flags = arg.slice(1).split(\"\");\n flags.forEach((flag: any) => {\n args[flag] = true;\n });\n }\n return args;\n }, {});\n\n// Network configurations\nconst networkConfigs: Record<NetworkType, NetworkConfig> = {\n aeneid: {\n rpcProviderUrl: 'https://aeneid.storyrpc.io',\n blockExplorer: 'https://aeneid.storyscan.io',\n protocolExplorer: 'https://aeneid.explorer.story.foundation',\n defaultNFTContractAddress: '0x937bef10ba6fb941ed84b8d249abc76031429a9a' as Address,\n defaultSPGNFTContractAddress: '0xc32A8a0FF3beDDDa58393d022aF433e78739FAbc' as Address,\n chain: aeneid,\n },\n mainnet: {\n rpcProviderUrl: 'https://mainnet.storyrpc.io',\n blockExplorer: 'https://storyscan.io',\n protocolExplorer: 'https://explorer.story.foundation',\n defaultNFTContractAddress: null,\n defaultSPGNFTContractAddress: '0x98971c660ac20880b60F86Cc3113eBd979eb3aAE' as Address,\n chain: mainnet,\n },\n} as const;\n\nconst getNetwork = (): NetworkType => {\n\n const args = getArgs();\n const network = ((args?.story_network || process.env.STORY_NETWORK) || 'aeneid') as NetworkType;\n\n if (network && !(network in networkConfigs)) {\n throw new Error(`Invalid network: ${network}. Must be one of: ${Object.keys(networkConfigs).join(', ')}`);\n }\n return network || 'aeneid';\n};\n\nconst getAccount = (): Account => {\n\n const args = getArgs();\n const hasPrivateKey = !!(args?.wallet_private_key || process.env.WALLET_PRIVATE_KEY);\n\n if (!hasPrivateKey) {\n throw new Error('WALLET_PRIVATE_KEY environment variable is required');\n }\n\n return privateKeyToAccount(`0x${(args?.wallet_private_key || process.env.WALLET_PRIVATE_KEY)}` as Address);\n}\n\n// Initialize client configuration\nexport const network = getNetwork();\n\nexport const networkInfo = {\n ...networkConfigs[network],\n rpcProviderUrl: networkConfigs[network].rpcProviderUrl,\n};\n\nexport const account: Account = getAccount()\n\nconst config: StoryConfig = {\n account,\n transport: http(networkInfo.rpcProviderUrl),\n chainId: network,\n};\n\nexport const client = StoryClient.newClient(config);\n\nconst baseConfig = {\n chain: networkInfo.chain,\n transport: http(networkInfo.rpcProviderUrl),\n} as const;\n\nexport const publicClient = createPublicClient(baseConfig);\n\nexport const walletClient = createWalletClient({\n ...baseConfig,\n account,\n}) as WalletClient;\n\nexport function validateEnvironment(): void {\n try {\n getAccount()\n getNetwork()\n console.error(`✅ Story Protocol environment configuration valid (${network})`);\n console.error(`📍 RPC URL: ${networkInfo.rpcProviderUrl}`);\n console.error(`📍 Account: ${account.address}`);\n } catch (error) {\n console.error('❌ Invalid environment configuration:', error);\n throw error;\n }\n}\n","#!/usr/bin/env node\n\nimport { validateEnvironment } from './config';\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { StoryMcpTools, getImplementationStatus } from \"./mcp\";\nimport { StoryAgent } from './agent';\n\n/**\n * Creates an MCP server for IP registration and licensing on Story Protocol\n */\nfunction createMcpServer(agent: StoryAgent) {\n // Create MCP server instance\n const server = new McpServer({\n name: \"story-build\",\n version: \"0.1.0\"\n });\n\n // Register all Story Protocol tools\n for (const [_key, tool] of Object.entries(StoryMcpTools)) {\n server.tool(tool.name, tool.description, tool.schema, async (params: any): Promise<any> => {\n try {\n // Execute the handler with the params directly\n const result = await tool.handler(agent, params);\n\n // Format the result as MCP tool response\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result, null, 2),\n },\n ],\n };\n } catch (error) {\n console.error(\"Tool execution error:\", error);\n // Handle errors in MCP format\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: error instanceof Error\n ? error.message\n : \"Unknown error occurred\",\n },\n ],\n };\n }\n });\n }\n\n return server;\n}\n\nasync function main() {\n try {\n console.error(\"🎨 Starting Story.build MCP Server...\");\n\n // Validate environment before proceeding\n validateEnvironment();\n\n // Create Story agent\n const storyAgent = new StoryAgent();\n\n // Get implementation status\n const status = getImplementationStatus();\n console.error(`📊 Implementation Status: ${status.implemented}/${status.total_defined} tools (${status.completion_percentage}%)`);\n\n // Create and start MCP server\n const server = createMcpServer(storyAgent);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n console.error(\"✅ Story.build MCP Server is running!\");\n console.error(\"🎯 IP registration and licensing tools ready!\");\n \n \n } catch (error) {\n console.error('❌ Error starting Story.build MCP server:', error);\n process.exit(1);\n }\n}\n\nmain();","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const GetWalletInfoTool: McpTool = {\n name: \"story_get_wallet_info\",\n description: \"Get wallet address and basic account information\",\n schema: {},\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n // const walletInfo = await agent.getWalletInfo();\n const balance = await agent.publicClient.getBalance({\n address: agent.account.address\n });\n \n const balanceInETH = Number(balance) / 1e18;\n\n return {\n status: \"success\",\n message: \"✅ Wallet information retrieved successfully\",\n wallet_details: {\n address: agent.account.address,\n network: agent.network,\n balance: `${balanceInETH.toFixed(6)} IP`,\n balance_in_wei: balance.toString(),\n chain_id: await agent.publicClient.getChainId(),\n block_explorer: agent.networkInfo.blockExplorer,\n protocol_explorer: agent.networkInfo.protocolExplorer\n },\n account_status: {\n activated: true,\n minimum_balance_required: \"0.01 IP\",\n can_register_ip: balanceInETH >= 0.01,\n ready_for_operations: balanceInETH >= 0.001\n },\n recommendations: balanceInETH < 0.01 \n ? [\n \"⚠️ Low IP balance detected\",\n \"Fund wallet with at least 0.01 IP for IP registration\",\n \"Gas fees required for all Story Protocol operations\",\n `Current balance: ${balanceInETH.toFixed(6)} IP`\n ]\n : [\n \"✅ Wallet has sufficient balance for operations\",\n \"Ready to register IP assets\",\n \"Ready to create license terms and mint tokens\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to get wallet info: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { formatEther, Address } from \"viem\";\nimport { WIP_TOKEN_ADDRESS } from \"@story-protocol/core-sdk\";\n\nexport const GetAccountBalancesTool: McpTool = {\n name: \"story_get_account_balances\",\n description: \"Get all token balances including IP, WIP tokens, and license tokens\",\n schema: {\n account_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .optional()\n .describe(\"Ethereum address to check (optional, defaults to wallet address)\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const targetAddress = (input.account_address || agent.account.address) as Address;\n\n // Get ETH balance\n const ethBalance = await agent.publicClient.getBalance({\n address: targetAddress\n });\n\n // Get WIP token balance (Story Protocol's wrapped token)\n let wipBalance = BigInt(0);\n try {\n wipBalance = await agent.publicClient.readContract({\n address: WIP_TOKEN_ADDRESS,\n abi: [\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n }\n ],\n functionName: 'balanceOf',\n args: [targetAddress]\n });\n } catch (error) {\n console.error('Error fetching WIP balance:', error);\n } \n\n return {\n status: \"success\",\n message: `✅ Account balances retrieved for ${targetAddress}`,\n account_info: {\n address: targetAddress,\n network: agent.network,\n is_own_wallet: targetAddress.toLowerCase() === agent.account.address.toLowerCase()\n },\n native_balance: {\n symbol: \"IP\",\n balance: formatEther(ethBalance),\n balance_wei: ethBalance.toString(),\n usd_value: \"N/A\" // Could integrate price feeds later\n },\n story_protocol_tokens: {\n wip: {\n symbol: \"WIP\",\n name: \"Wrapped IP Token\",\n balance: formatEther(wipBalance),\n balance_wei: wipBalance.toString(),\n contract_address: WIP_TOKEN_ADDRESS,\n purpose: \"Used for licensing fees and royalty payments\"\n }\n },\n portfolio_summary: {\n total_ip_balance: formatEther(ethBalance),\n total_wip_balance: formatEther(wipBalance),\n can_pay_gas: Number(formatEther(ethBalance)) > 0.001,\n can_pay_licensing_fees: wipBalance > 0,\n ready_for_ip_operations: Number(formatEther(ethBalance)) > 0.001\n },\n next_steps: Number(formatEther(ethBalance)) < 0.001\n ? [\n \"🔋 Fund wallet with ETH for gas fees\",\n \"💎 Acquire WIP tokens for licensing operations\",\n \"🎨 Ready to register IP assets once funded\"\n ]\n : [\n \"✅ Sufficient ETH for gas fees\",\n \"🎨 Ready to register IP assets\",\n \"🎫 Ready to mint and purchase licenses\",\n wipBalance === BigInt(0) ? \"💡 Consider acquiring WIP tokens for licensing\" : \"💎 WIP tokens available for licensing\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to get account balances: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { parseEther, Address } from \"viem\";\n\nexport const SendETHTool: McpTool = {\n name: \"story_send_native_ip\",\n description: \"Send native IP token to another address for gas fees or payments\",\n schema: {\n destination: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Recipient's Ethereum address\"),\n amount: z.number()\n .positive()\n .describe(\"Amount of IP to send\"),\n memo: z.string()\n .optional()\n .describe(\"Optional memo for the transaction\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const destination = input.destination as Address;\n const amount = parseEther(input.amount.toString());\n \n // Check sender balance\n const balance = await agent.publicClient.getBalance({\n address: agent.account.address\n });\n\n if (balance < amount) {\n throw new Error(`Insufficient balance. Available: ${Number(balance) / 1e18} IP, Required: ${input.amount} IP`);\n }\n\n // Simulate transaction first to get accurate gas estimate and catch errors\n let gasEstimate: bigint;\n try {\n // For ETH transfers, we use estimateGas directly since it's not a contract call\n gasEstimate = await agent.publicClient.estimateGas({\n account: agent.account.address,\n to: destination,\n value: amount\n });\n } catch (error: any) {\n throw new Error(`Transaction simulation failed: ${error.message}. Check recipient address and amount.`);\n }\n\n // Get gas price for cost calculation\n const gasPrice = await agent.publicClient.getGasPrice();\n const gasCost = gasEstimate * gasPrice;\n\n if (balance < amount + gasCost) {\n throw new Error(`Insufficient balance for transaction + gas. Total needed: ${Number(amount + gasCost) / 1e18} IP`);\n }\n\n console.error(`✅ Native IP transfer simulation successful. Gas estimate: ${gasEstimate.toString()}`);\n\n // Send transaction\n const txHash = await agent.walletClient.sendTransaction({\n account: agent.account,\n to: destination,\n value: amount,\n gas: gasEstimate\n } as any);\n\n // Wait for confirmation\n const receipt = await agent.publicClient.waitForTransactionReceipt({\n hash: txHash,\n confirmations: 1\n });\n\n return {\n status: \"success\",\n message: `✅ Successfully sent ${input.amount} IP to ${destination}`,\n transaction_details: {\n transaction_hash: txHash,\n from: agent.account.address,\n to: destination,\n amount: `${input.amount} IP`,\n amount_wei: amount.toString(),\n gas_used: receipt.gasUsed.toString(),\n gas_price: gasPrice.toString(),\n total_cost: `${Number(amount + (receipt.gasUsed * gasPrice)) / 1e18} IP`,\n block_number: receipt.blockNumber.toString(),\n confirmations: 1,\n memo: input.memo || \"N/A\"\n },\n network_info: {\n network: agent.network,\n explorer_url: `${agent.networkInfo.blockExplorer}/tx/${txHash}`\n },\n next_steps: [\n \"✅ Transaction confirmed on blockchain\",\n \"🔍 View transaction details on block explorer\",\n \"💰 Recipient can now use IP for Story Protocol operations\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to send IP: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { parseEther, Address, formatEther } from \"viem\";\nimport { WIP_TOKEN_ADDRESS } from \"@story-protocol/core-sdk\";\n\nexport const SendTokenTool: McpTool = {\n name: \"story_send_token\",\n description: \"Send Story Protocol tokens (WIP, or custom tokens) to another address\",\n schema: {\n token_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Token contract address (use 'WIP' for WIP token shortcut)\"),\n destination: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Recipient's Ethereum address\"),\n amount: z.number()\n .positive()\n .describe(\"Amount of tokens to send\"),\n memo: z.string()\n .optional()\n .describe(\"Optional memo for the transaction\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const destination = input.destination as Address;\n let tokenAddress = input.token_address as Address;\n \n // Handle shortcuts\n if (input.token_address === \"WIP\") {\n tokenAddress = WIP_TOKEN_ADDRESS;\n } \n\n const amount = parseEther(input.amount.toString());\n\n // ERC20 ABI for token operations\n const erc20Abi = [\n {\n name: 'transfer',\n type: 'function',\n stateMutability: 'nonpayable',\n inputs: [\n { name: 'to', type: 'address' },\n { name: 'amount', type: 'uint256' }\n ],\n outputs: [{ name: '', type: 'bool' }],\n },\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'symbol',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n },\n {\n name: 'decimals',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'uint8' }],\n }\n ];\n\n // Get token info\n const [balance, symbol, decimals] = await Promise.all([\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [agent.account.address]\n }) as Promise<bigint>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'symbol'\n }) as Promise<string>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'decimals'\n }) as Promise<number>\n ]);\n\n if (balance < amount) {\n throw new Error(`Insufficient ${symbol} balance. Available: ${formatEther(balance)}, Required: ${input.amount}`);\n }\n\n // Simulate token transfer first to catch errors and get accurate gas estimate\n const { request, result } = await agent.publicClient.simulateContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'transfer',\n args: [destination, amount],\n account: agent.account.address\n });\n\n console.error(`✅ Transfer simulation successful. Proceeding with transaction...`);\n\n // Send token transfer transaction using the simulated request\n const txHash = await agent.walletClient.writeContract(request);\n\n // Wait for confirmation\n const receipt = await agent.publicClient.waitForTransactionReceipt({\n hash: txHash,\n confirmations: 1\n });\n\n return {\n status: \"success\",\n message: `✅ Successfully sent ${input.amount} ${symbol} to ${destination}`,\n transaction_details: {\n transaction_hash: txHash,\n from: agent.account.address,\n to: destination,\n token_address: tokenAddress,\n token_symbol: symbol,\n amount: `${input.amount} ${symbol}`,\n amount_wei: amount.toString(),\n decimals: decimals,\n gas_used: receipt.gasUsed.toString(),\n block_number: receipt.blockNumber.toString(),\n confirmations: 1,\n memo: input.memo || \"N/A\"\n },\n token_info: {\n contract_address: tokenAddress,\n symbol: symbol,\n decimals: decimals,\n is_story_protocol_token: tokenAddress === WIP_TOKEN_ADDRESS\n },\n network_info: {\n network: agent.network,\n explorer_url: `${agent.networkInfo.blockExplorer}/tx/${txHash}`\n },\n next_steps: [\n \"✅ Token transfer confirmed on blockchain\",\n \"🔍 View transaction details on block explorer\",\n `💎 Recipient can now use ${symbol} tokens for Story Protocol operations`\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to send tokens: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { parseEther, Address, formatEther, maxUint256 } from \"viem\";\nimport { WIP_TOKEN_ADDRESS } from \"@story-protocol/core-sdk\";\n\nexport const ApproveTokenTool: McpTool = {\n name: \"story_approve_token\",\n description: \"Approve Story Protocol contracts to spend your tokens (required for licensing operations)\",\n schema: {\n token_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Token contract address (use 'WIP' for WIP token shortcut)\"),\n spender: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Contract address to approve (Story Protocol contracts)\"),\n amount: z.number()\n .positive()\n .optional()\n .describe(\"Amount to approve (optional, defaults to unlimited)\"),\n unlimited: z.boolean()\n .default(true)\n .describe(\"Set unlimited approval (recommended for convenience)\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n let tokenAddress = input.token_address as Address;\n const spender = input.spender as Address;\n \n // Handle shortcuts\n if (input.token_address === \"WIP\") {\n tokenAddress = WIP_TOKEN_ADDRESS;\n }\n\n const amount = input.unlimited || !input.amount \n ? maxUint256 \n : parseEther(input.amount.toString());\n\n // ERC20 ABI for approval operations\n const erc20Abi = [\n {\n name: 'approve',\n type: 'function',\n stateMutability: 'nonpayable',\n inputs: [\n { name: 'spender', type: 'address' },\n { name: 'amount', type: 'uint256' }\n ],\n outputs: [{ name: '', type: 'bool' }],\n },\n {\n name: 'allowance',\n type: 'function',\n stateMutability: 'view',\n inputs: [\n { name: 'owner', type: 'address' },\n { name: 'spender', type: 'address' }\n ],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'symbol',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n },\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n }\n ];\n\n // Get current allowance and token info\n const [currentAllowance, symbol, balance] = await Promise.all([\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'allowance',\n args: [agent.account.address, spender]\n }) as Promise<bigint>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'symbol'\n }) as Promise<string>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [agent.account.address]\n }) as Promise<bigint>\n ]);\n\n // Check if approval is needed\n if (currentAllowance >= amount && amount !== maxUint256) {\n return {\n status: \"success\",\n message: `✅ Sufficient approval already exists for ${symbol}`,\n approval_details: {\n token_address: tokenAddress,\n token_symbol: symbol,\n spender: spender,\n current_allowance: input.unlimited ? \"Unlimited\" : formatEther(currentAllowance),\n requested_amount: input.unlimited ? \"Unlimited\" : input.amount?.toString(),\n approval_needed: false\n },\n wallet_info: {\n balance: formatEther(balance),\n address: agent.account.address\n },\n next_steps: [\n \"✅ Approval already sufficient\",\n \"🎨 Ready to proceed with Story Protocol operations\",\n \"💡 No additional transaction needed\"\n ]\n };\n }\n\n // Simulate approval first to catch errors and get accurate gas estimate\n const { request, result } = await agent.publicClient.simulateContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'approve',\n args: [spender, amount],\n account: agent.account.address\n });\n\n console.error(`✅ Approval simulation successful. Proceeding with transaction...`);\n\n // Send approval transaction using the simulated request\n const txHash = await agent.walletClient.writeContract(request);\n\n // Wait for confirmation\n const receipt = await agent.publicClient.waitForTransactionReceipt({\n hash: txHash,\n confirmations: 1\n });\n\n // Get new allowance\n const newAllowance = await agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'allowance',\n args: [agent.account.address, spender]\n }) as bigint;\n\n return {\n status: \"success\",\n message: `✅ Successfully approved ${symbol} spending for Story Protocol contract`,\n transaction_details: {\n transaction_hash: txHash,\n from: agent.account.address,\n token_address: tokenAddress,\n token_symbol: symbol,\n spender: spender,\n approved_amount: input.unlimited ? \"Unlimited\" : input.amount?.toString(),\n gas_used: receipt.gasUsed.toString(),\n block_number: receipt.blockNumber.toString(),\n confirmations: 1\n },\n approval_details: {\n previous_allowance: formatEther(currentAllowance),\n new_allowance: amount === maxUint256 ? \"Unlimited\" : formatEther(newAllowance),\n is_unlimited: amount === maxUint256,\n spender_contract: spender\n },\n wallet_info: {\n balance: formatEther(balance),\n address: agent.account.address\n },\n network_info: {\n network: agent.network,\n explorer_url: `${agent.networkInfo.blockExplorer}/tx/${txHash}`\n },\n next_steps: [\n \"✅ Token approval confirmed on blockchain\",\n \"🎨 Ready to register IP assets and mint licenses\",\n \"💎 Story Protocol contracts can now spend your tokens\",\n \"🔍 View transaction details on block explorer\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to approve tokens: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { Address, formatEther } from \"viem\";\nimport { WIP_TOKEN_ADDRESS } from \"@story-protocol/core-sdk\";\n\nexport const CheckAllowanceTool: McpTool = {\n name: \"story_check_allowance\",\n description: \"Check token allowance for Story Protocol contracts and other spenders\",\n schema: {\n token_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Token contract address (use 'WIP' for WIP token shortcut)\"),\n owner: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .optional()\n .describe(\"Token owner address (optional, defaults to wallet address)\"),\n spender: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Spender contract address to check allowance for\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n let tokenAddress = input.token_address as Address;\n const owner = (input.owner || agent.account.address) as Address;\n const spender = input.spender as Address;\n \n // Handle shortcuts\n if (input.token_address === \"WIP\") {\n tokenAddress = WIP_TOKEN_ADDRESS;\n }\n\n // ERC20 ABI for allowance operations\n const erc20Abi = [\n {\n name: 'allowance',\n type: 'function',\n stateMutability: 'view',\n inputs: [\n { name: 'owner', type: 'address' },\n { name: 'spender', type: 'address' }\n ],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'symbol',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n },\n {\n name: 'decimals',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'uint8' }],\n },\n {\n name: 'name',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n }\n ];\n\n // Get token info and allowance\n const [allowance, balance, symbol, decimals, name] = await Promise.all([\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'allowance',\n args: [owner, spender]\n }) as Promise<bigint>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [owner]\n }) as Promise<bigint>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'symbol'\n }) as Promise<string>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'decimals'\n }) as Promise<number>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'name'\n }) as Promise<string>\n ]);\n\n // Check if allowance is unlimited (max uint256)\n const maxUint256 = BigInt(\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\");\n const isUnlimited = allowance >= maxUint256 / BigInt(2); // Close to max value\n\n // Calculate allowance status\n const allowanceFormatted = isUnlimited ? \"Unlimited\" : formatEther(allowance);\n const balanceFormatted = formatEther(balance);\n const canSpendBalance = allowance >= balance;\n const needsApproval = allowance === BigInt(0);\n\n // Determine approval recommendations\n const getRecommendations = () => {\n if (needsApproval) {\n return [\n \"⚠️ No allowance set - approval required\",\n `Use story_approve_token to approve ${symbol} spending`,\n \"Recommend setting unlimited approval for convenience\"\n ];\n } else if (!canSpendBalance && !isUnlimited) {\n return [\n \"⚠️ Allowance is less than current balance\",\n `Current allowance: ${allowanceFormatted} ${symbol}`,\n `Current balance: ${balanceFormatted} ${symbol}`,\n \"Consider increasing allowance for full balance access\"\n ];\n } else if (isUnlimited) {\n return [\n \"✅ Unlimited allowance set\",\n \"No further approvals needed for this token\",\n \"Ready for all Story Protocol operations\"\n ];\n } else {\n return [\n \"✅ Sufficient allowance for current balance\",\n `Can spend up to ${allowanceFormatted} ${symbol}`,\n \"Ready for Story Protocol operations\"\n ];\n }\n };\n\n return {\n status: \"success\",\n message: `✅ Allowance checked for ${symbol} token`,\n allowance_details: {\n token_address: tokenAddress,\n token_name: name,\n token_symbol: symbol,\n token_decimals: decimals,\n owner: owner,\n spender: spender,\n allowance: allowanceFormatted,\n allowance_wei: allowance.toString(),\n is_unlimited: isUnlimited,\n is_zero: allowance === BigInt(0)\n },\n balance_comparison: {\n owner_balance: balanceFormatted,\n owner_balance_wei: balance.toString(),\n can_spend_full_balance: canSpendBalance,\n allowance_vs_balance: allowance >= balance ? \"sufficient\" : \"insufficient\"\n },\n contract_info: {\n is_story_protocol_token: tokenAddress === WIP_TOKEN_ADDRESS,\n spender_contract: spender,\n is_own_wallet: owner.toLowerCase() === agent.account.address.toLowerCase()\n },\n operational_status: {\n needs_approval: needsApproval,\n ready_for_operations: !needsApproval,\n can_spend_tokens: allowance > 0,\n approval_sufficient: canSpendBalance || isUnlimited\n },\n network_info: {\n network: agent.network,\n block_explorer: agent.networkInfo.blockExplorer,\n token_explorer_url: `${agent.networkInfo.blockExplorer}/token/${tokenAddress}`,\n approval_explorer_url: `${agent.networkInfo.blockExplorer}/token/${tokenAddress}?a=${owner}`\n },\n recommendations: getRecommendations(),\n next_steps: needsApproval \n ? [\n `🔧 Run: story_approve_token with token_address=${symbol === 'WIP' ? 'WIP' : tokenAddress}`,\n `📄 Specify spender=${spender}`,\n \"💡 Consider unlimited approval for convenience\",\n \"🎨 Then proceed with Story Protocol operations\"\n ]\n : [\n \"✅ Allowance is properly configured\",\n \"🎨 Ready to proceed with Story Protocol operations\",\n `💎 Can spend ${canSpendBalance ? 'full balance' : 'partial balance'} of ${symbol}`,\n \"🔍 View token details on block explorer\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to check allowance: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};","import { z } from \"zod\";\nimport { StoryAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\nimport { Address, formatEther } from \"viem\";\nimport { WIP_TOKEN_ADDRESS } from \"@story-protocol/core-sdk\";\n\nexport const GetTokenInfoTool: McpTool = {\n name: \"story_get_token_info\",\n description: \"Get comprehensive information about ERC20 tokens including metadata and user balances\",\n schema: {\n token_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .describe(\"Token contract address (use 'WIP' for WIP token shortcut)\"),\n account_address: z.string()\n .regex(/^0x[0-9a-fA-F]{40}$/)\n .optional()\n .describe(\"Address to check balance for (optional, defaults to wallet address)\")\n },\n handler: async (agent: StoryAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n let tokenAddress = input.token_address as Address;\n const accountAddress = (input.account_address || agent.account.address) as Address;\n \n // Handle shortcuts\n if (input.token_address === \"WIP\") {\n tokenAddress = WIP_TOKEN_ADDRESS;\n }\n\n // Extended ERC20 ABI for comprehensive token info\n const erc20Abi = [\n {\n name: 'name',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n },\n {\n name: 'symbol',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'string' }],\n },\n {\n name: 'decimals',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'uint8' }],\n },\n {\n name: 'totalSupply',\n type: 'function',\n stateMutability: 'view',\n inputs: [],\n outputs: [{ name: '', type: 'uint256' }],\n },\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n }\n ];\n\n // Get basic token information\n const [name, symbol, decimals, totalSupply, balance] = await Promise.all([\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'name'\n }) as Promise<string>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'symbol'\n }) as Promise<string>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'decimals'\n }) as Promise<number>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'totalSupply'\n }) as Promise<bigint>,\n agent.publicClient.readContract({\n address: tokenAddress,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [accountAddress]\n }) as Promise<bigint>\n ]);\n\n // Get contract bytecode to verify it's a contract\n const bytecode = await agent.publicClient.getBytecode({\n address: tokenAddress\n });\n\n const isContract = !!(bytecode && bytecode !== '0x');\n\n // Calculate formatted values\n const totalSupplyFormatted = formatEther(totalSupply);\n const balanceFormatted = formatEther(balance);\n const balancePercentage = totalSupply > 0 ? (Number(balance) / Number(totalSupply)) * 100 : 0;\n\n // Determine token type and purpose\n const getTokenInfo = () => {\n if (tokenAddress === WIP_TOKEN_ADDRESS) {\n return {\n type: \"story_protocol_token\",\n category: \"wrapped_token\",\n purpose: \"Used for licensing fees and royalty payments on Story Protocol\",\n official: true\n };\n } else {\n return {\n type: \"erc20_token\",\n category: \"custom\",\n purpose: \"Custom ERC20 token - verify legitimacy before use\",\n official: false\n };\n }\n };\n\n const tokenInfo = getTokenInfo();\n\n return {\n status: \"success\",\n message: `✅ Token information retrieved for ${symbol}`,\n token_metadata: {\n contract_address: tokenAddress,\n name: name,\n symbol: symbol,\n decimals: decimals,\n total_supply: totalSupplyFormatted,\n total_supply_wei: totalSupply.toString(),\n is_contract: isContract,\n ...tokenInfo\n },\n account_balance: {\n address: accountAddress,\n balance: balanceFormatted,\n balance_wei: balance.toString(),\n percentage_of_supply: balancePercentage.toFixed(6) + \"%\",\n is_holder: balance > 0,\n is_own_wallet: accountAddress.toLowerCase() === agent.account.address.toLowerCase()\n },\n supply_analysis: {\n total_supply_formatted: totalSupplyFormatted,\n user_balance_formatted: balanceFormatted,\n supply_concentration: balancePercentage > 1 ? \"significant_holder\" : \n balancePercentage > 0.1 ? \"moderate_holder\" : \n balancePercentage