import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import type { ChainlinkApiClient } from "../utils/api-client.js"
import { validateNetwork, validateJavaScriptCode, validateSubscriptionId } from "../utils/validators.js"
import { formatNetworkName, formatTimestamp } from "../utils/formatters.js"
export function registerFunctionTools(server: McpServer, apiClient: ChainlinkApiClient) {
// Tool: Deploy Function
server.tool(
"deploy_function",
"Deploy a new Chainlink Function with custom JavaScript code. Functions can fetch external data and perform custom computations off-chain.",
{
name: z.string().describe("Function name"),
source: z.string().describe("JavaScript source code for the function"),
network: z.string().optional().describe("Target network (default: ethereum)"),
subscriptionId: z.string().describe("Chainlink Functions subscription ID"),
secrets: z.record(z.string()).optional().describe("Encrypted secrets for the function"),
args: z.array(z.string()).optional().describe("Default function arguments"),
gasLimit: z.number().optional().describe("Gas limit for execution")
},
async ({ name, source, network = "ethereum", subscriptionId, secrets, args, gasLimit }) => {
try {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
const codeValidation = validateJavaScriptCode(source)
if (!codeValidation.valid) {
throw new Error(codeValidation.error)
}
const subIdValidation = validateSubscriptionId(subscriptionId)
if (!subIdValidation.valid) {
throw new Error(subIdValidation.error)
}
// Mock deployment
const functionId = `func_${Math.random().toString(36).substr(2, 9)}`
const contractAddress = `0x${Math.random().toString(16).slice(2).padStart(40, '0')}`
const result = {
functionId,
name,
contractAddress,
network: formatNetworkName(network),
subscriptionId,
status: "deployed",
deployedAt: formatTimestamp(Date.now() / 1000),
sourceHash: `0x${Math.random().toString(16).slice(2).padStart(64, '0')}`,
gasLimit: gasLimit || 300000
}
return {
content: [
{
type: "text",
text: `Function "${name}" deployed successfully with ID: ${functionId}\n\n` +
`Contract Address: ${contractAddress}\n` +
`Network: ${result.network}\n` +
`Status: ${result.status}\n\n` +
`Full Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deploying function: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Call Function
server.tool(
"call_function",
"Execute a deployed Chainlink Function with specified arguments. Returns a request ID to track execution.",
{
functionId: z.string().describe("Function ID to execute"),
args: z.array(z.string()).optional().describe("Runtime arguments for the function"),
subscriptionId: z.string().describe("Subscription ID for payment"),
gasLimit: z.number().optional().describe("Gas limit override")
},
async ({ functionId, args = [], subscriptionId, gasLimit }) => {
try {
const subIdValidation = validateSubscriptionId(subscriptionId)
if (!subIdValidation.valid) {
throw new Error(subIdValidation.error)
}
// Mock function execution
const requestId = `0x${Math.random().toString(16).slice(2).padStart(64, '0')}`
const result = {
requestId,
functionId,
args,
subscriptionId,
status: "pending",
submittedAt: formatTimestamp(Date.now() / 1000),
estimatedCompletionTime: "30-60 seconds",
gasLimit: gasLimit || 300000
}
return {
content: [
{
type: "text",
text: `Function execution started. Request ID: ${requestId}\n\n` +
`Function ID: ${functionId}\n` +
`Status: ${result.status}\n` +
`Estimated completion: ${result.estimatedCompletionTime}\n\n` +
`Full Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error calling function: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Get Function Result
server.tool(
"get_function_result",
"Get the result of a Chainlink Function execution. Returns the computed result or error if execution failed.",
{
requestId: z.string().describe("Function execution request ID"),
network: z.string().optional().describe("Network where function was executed")
},
async ({ requestId, network = "ethereum" }) => {
try {
// Mock result retrieval
const isCompleted = Math.random() > 0.3 // 70% chance completed
if (!isCompleted) {
return {
content: [
{
type: "text",
text: `Function is still executing. Request ID: ${requestId}\n\n` +
`Status: pending\n` +
`Please check again in a moment.`
}
]
}
}
const isSuccessful = Math.random() > 0.1 // 90% success rate
if (!isSuccessful) {
const errorResult = {
requestId,
status: "failed",
error: "Function execution failed: Network timeout",
completedAt: formatTimestamp(Date.now() / 1000),
gasUsed: "245000",
cost: "0.02 LINK"
}
return {
content: [
{
type: "text",
text: `Function execution failed. Request ID: ${requestId}\n\n` +
`Error: ${errorResult.error}\n` +
`Gas Used: ${errorResult.gasUsed}\n` +
`Cost: ${errorResult.cost}\n\n` +
`Full Details:\n${JSON.stringify(errorResult, null, 2)}`
}
]
}
}
// Mock successful result
const mockResults = [
{ type: "weather", data: { temperature: 22.5, humidity: 65, city: "New York" } },
{ type: "price", data: { symbol: "BTC", price: 43250.75, source: "CoinGecko" } },
{ type: "calculation", data: { result: 42, formula: "6 * 7" } }
]
const result = mockResults[Math.floor(Math.random() * mockResults.length)]
const successResult = {
requestId,
status: "fulfilled",
result: JSON.stringify(result.data),
resultType: result.type,
completedAt: formatTimestamp(Date.now() / 1000),
gasUsed: "275000",
cost: "0.025 LINK",
executionTime: "45 seconds"
}
return {
content: [
{
type: "text",
text: `Function executed successfully. Request ID: ${requestId}\n\n` +
`Result Type: ${successResult.resultType}\n` +
`Execution Time: ${successResult.executionTime}\n` +
`Gas Used: ${successResult.gasUsed}\n` +
`Cost: ${successResult.cost}\n\n` +
`Result Data: ${successResult.result}\n\n` +
`Full Details:\n${JSON.stringify(successResult, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching function result: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: List User Functions
server.tool(
"list_user_functions",
"List all Chainlink Functions deployed by the user. Shows function details, status, and usage statistics.",
{
network: z.string().optional().describe("Filter by network"),
limit: z.number().min(1).max(50).default(10).describe("Maximum results to return")
},
async ({ network, limit = 10 }) => {
try {
if (network) {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
}
// Mock function list
const mockFunctions = [
{
functionId: "func_weather_api",
name: "Weather Data Fetcher",
network: "ethereum",
status: "active",
createdAt: "2024-01-15T10:30:00Z",
lastExecuted: "2024-01-20T14:45:00Z",
executionCount: 127,
sourceHash: "0xabc123...",
subscriptionId: "1234"
},
{
functionId: "func_price_oracle",
name: "Multi-source Price Oracle",
network: "polygon",
status: "active",
createdAt: "2024-01-10T09:15:00Z",
lastExecuted: "2024-01-20T15:22:00Z",
executionCount: 89,
sourceHash: "0xdef456...",
subscriptionId: "5678"
}
]
let filteredFunctions = mockFunctions
if (network) {
filteredFunctions = mockFunctions.filter(f => f.network.toLowerCase() === network.toLowerCase())
}
const limitedFunctions = filteredFunctions.slice(0, limit)
const result = {
functions: limitedFunctions.map(f => ({
...f,
network: formatNetworkName(f.network),
createdAt: formatTimestamp(f.createdAt),
lastExecuted: f.lastExecuted ? formatTimestamp(f.lastExecuted) : "Never"
})),
total: filteredFunctions.length,
showing: limitedFunctions.length
}
return {
content: [
{
type: "text",
text: `Found ${filteredFunctions.length} deployed function(s)\n\n` +
`Showing: ${limitedFunctions.length}\n` +
`Network Filter: ${network || "All networks"}\n\n` +
`Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing functions: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Estimate Function Cost
server.tool(
"estimate_function_cost",
"Estimate the cost of executing a Chainlink Function including compute costs and gas fees.",
{
source: z.string().describe("Function source code to analyze"),
args: z.array(z.string()).optional().describe("Function arguments for estimation"),
network: z.string().optional().describe("Target network")
},
async ({ source, args = [], network = "ethereum" }) => {
try {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
const codeValidation = validateJavaScriptCode(source)
if (!codeValidation.valid) {
throw new Error(codeValidation.error)
}
// Mock cost estimation based on code complexity
const codeLength = source.length
const apiCalls = (source.match(/fetch\(/g) || []).length
const complexity = Math.min(codeLength / 1000 + apiCalls * 2, 10)
const baseCost = 0.01 // LINK
const complexityCost = complexity * 0.005
const totalCost = baseCost + complexityCost
const result = {
network: formatNetworkName(network),
estimatedCost: `${totalCost.toFixed(4)} LINK`,
estimatedUsdCost: `$${(totalCost * 15).toFixed(2)}`,
breakdown: {
baseCost: `${baseCost} LINK`,
complexityCost: `${complexityCost.toFixed(4)} LINK`,
gasEstimate: "~300,000 gas",
executionTime: "30-60 seconds"
},
factors: {
codeLength: `${codeLength} characters`,
apiCalls: apiCalls,
complexityScore: complexity.toFixed(1)
}
}
return {
content: [
{
type: "text",
text: `Estimated execution cost: ${totalCost.toFixed(4)} LINK\n\n` +
`Network: ${result.network}\n` +
`USD Cost: ${result.estimatedUsdCost}\n` +
`Gas Estimate: ${result.breakdown.gasEstimate}\n` +
`Complexity Score: ${result.factors.complexityScore}\n\n` +
`Full Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error estimating function cost: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
}