Skip to main content
Glama

phalcon-mcp

by mark3labs
server.go17.3 kB
package server import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/cookiejar" "strconv" "strings" "sync" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" ) // Server represents the Phalcon MCP server type Server struct { mcpServer *mcpserver.MCPServer version string } // NewServer creates a new Phalcon MCP server func NewServer(version string) *Server { s := &Server{ mcpServer: mcpserver.NewMCPServer( "Phalcon MCP", version, ), version: version, } // Add trace tool traceTool := mcp.NewTool("trace", mcp.WithDescription("Trace the different calls of a transaction on a blockchain also provide gas usage metrics."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to trace"), ), ) // Add profile tool profileTool := mcp.NewTool("profile", mcp.WithDescription("Profile a transaction on a blockchain. Gives details about the transaction, flow of funds and token information."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to trace"), ), ) // Add address-label tool addressLabelTool := mcp.NewTool("address-label", mcp.WithDescription("Get human readable labels for contract addresses like tokens, protocols, and other on-chain entities."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to get address labels for"), ), ) // Add balance-change tool balanceChangeTool := mcp.NewTool("balance-change", mcp.WithDescription("Retrieve detailed balance change information for a transaction."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to get balance changes for"), ), ) // Add state-change tool stateChangeTool := mcp.NewTool("state-change", mcp.WithDescription("Retrieve detailed information about state changes like storage variables in contracts for a transaction."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to get state changes for"), ), ) // Add transaction-overview tool transactionOverviewTool := mcp.NewTool("transaction-overview", mcp.WithDescription("Comprehensive overview of a transaction by aggregating data from all available analysis tools."), mcp.WithString("chainId", mcp.Required(), mcp.Description("ID of the blockchain"), ), mcp.WithString("transactionHash", mcp.Required(), mcp.Description("Hash of the transaction to analyze"), ), ) // Add get-chain-id-by-name tool getChainIdTool := mcp.NewTool("get-chain-id-by-name", mcp.WithDescription("Get the chain ID for a blockchain by name, chain, or chainSlug"), mcp.WithString("name", mcp.Required(), mcp.Description("The name of the blockchain to look up"), ), ) // Add tool handlers s.mcpServer.AddTool(traceTool, s.traceHandler) s.mcpServer.AddTool(profileTool, s.profileHandler) s.mcpServer.AddTool(addressLabelTool, s.addressLabelHandler) s.mcpServer.AddTool(balanceChangeTool, s.balanceChangeHandler) s.mcpServer.AddTool(stateChangeTool, s.stateChangeHandler) s.mcpServer.AddTool(transactionOverviewTool, s.transactionOverviewHandler) s.mcpServer.AddTool(getChainIdTool, s.getChainIdByNameHandler) return s } // ServeStdio starts the MCP server in stdio mode func (s *Server) ServeStdio() error { return mcpserver.ServeStdio(s.mcpServer) } // GetMCPServer returns the underlying MCPServer for use with InProcessTransport func (s *Server) GetMCPServer() *mcpserver.MCPServer { return s.mcpServer } // BlocksecTraceRequest represents the request payload for BlockSec API type BlocksecTraceRequest struct { ChainID int `json:"chainID"` TxnHash string `json:"txnHash"` Blocked bool `json:"blocked"` } // extractRequestParams extracts and validates chainId and transactionHash from the request func extractRequestParams(request mcp.CallToolRequest) (int, string, error) { chainIdStr, err := request.RequireString("chainId") if err != nil { return 0, "", fmt.Errorf("chainId is required: %v", err) } // Convert chainId to integer chainId, err := strconv.Atoi(chainIdStr) if err != nil { return 0, "", fmt.Errorf("invalid chainId format: %v", err) } txHash, err := request.RequireString("transactionHash") if err != nil { return 0, "", fmt.Errorf("transactionHash is required: %v", err) } return chainId, txHash, nil } // createHTTPClient creates an HTTP client with a cookie jar and browser-like headers func createHTTPClient() (*http.Client, error) { // Create a cookiejar to store cookies jar, err := cookiejar.New(nil) if err != nil { return nil, fmt.Errorf("failed to create cookie jar: %v", err) } // Create HTTP client with the cookiejar client := &http.Client{ Jar: jar, } return client, nil } // fetchBlocksecCookies visits the main site to get cookies with retries func fetchBlocksecCookies(client *http.Client) error { maxRetries := 3 var lastErr error mainPageURL := "https://app.blocksec.com/explorer" for attempt := 0; attempt <= maxRetries; attempt++ { req, err := http.NewRequest("GET", mainPageURL, nil) if err != nil { return fmt.Errorf("failed to create request to main page: %v", err) } // Set browser-like headers req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") req.Header.Set("Accept-Language", "en-US,en;q=0.5") req.Header.Set("Connection", "keep-alive") req.Header.Set("Upgrade-Insecure-Requests", "1") // Get main page to retrieve cookies mainResp, err := client.Do(req) if err != nil { lastErr = fmt.Errorf("failed to send request to main site: %v", err) continue } // Check for successful response if mainResp.StatusCode != http.StatusOK { mainResp.Body.Close() lastErr = fmt.Errorf("main site returned non-200 status code: %d", mainResp.StatusCode) continue } mainResp.Body.Close() // We don't need the body return nil // Success } // If we've tried maxRetries times and still failed, return the last error return fmt.Errorf("failed to fetch cookies after %d attempts: %v", maxRetries, lastErr) } // callBlocksecAPI makes an API call to the BlockSec API with retries func callBlocksecAPI(client *http.Client, endpoint string, chainId int, txHash string) ([]byte, error) { // Configure retries maxRetries := 3 var lastErr error var respBody []byte for attempt := 0; attempt <= maxRetries; attempt++ { // Prepare the API request with cookies reqBody := BlocksecTraceRequest{ ChainID: chainId, TxnHash: txHash, Blocked: false, } // Convert request to JSON jsonData, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request: %v", err) } // Create a new request for the API apiURL := fmt.Sprintf("https://app.blocksec.com/api/v1/onchain/tx/%s", endpoint) apiReq, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create API request: %v", err) } // Set headers to mimic a browser for the API request apiReq.Header.Set("Content-Type", "application/json") apiReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36") apiReq.Header.Set("Accept", "application/json, text/plain, */*") apiReq.Header.Set("Origin", "https://app.blocksec.com") apiReq.Header.Set("Referer", "https://app.blocksec.com/explorer") // Send the API request resp, err := client.Do(apiReq) if err != nil { lastErr = fmt.Errorf("failed to send request to BlockSec API: %v", err) continue // Try again } // Read response body respBody, err = io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastErr = fmt.Errorf("failed to read response: %v", err) continue // Try again } // Check if the request was successful if resp.StatusCode == http.StatusOK { return respBody, nil // Success, return the response } // If we got here, the request failed with a non-200 status code lastErr = fmt.Errorf("BlockSec API returned non-200 status code: %d - %s", resp.StatusCode, string(respBody)) } // If we've tried maxRetries times and still failed, return the last error return nil, fmt.Errorf("failed after %d attempts: %v", maxRetries, lastErr) } // formatJSONResponse formats the response as compact JSON func formatJSONResponse(respBody []byte) (*mcp.CallToolResult, error) { // Return the raw JSON without indentation return mcp.NewToolResultText(string(respBody)), nil } // handleBlocksecRequest handles all BlockSec API requests using shared code func (s *Server) handleBlocksecRequest(ctx context.Context, request mcp.CallToolRequest, endpoint string) (*mcp.CallToolResult, error) { // Extract and validate parameters chainId, txHash, err := extractRequestParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Create HTTP client client, err := createHTTPClient() if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Fetch cookies if err := fetchBlocksecCookies(client); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Call the API respBody, err := callBlocksecAPI(client, endpoint, chainId, txHash) if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Format and return the response return formatJSONResponse(respBody) } // traceHandler handles the trace tool requests func (s *Server) traceHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return s.handleBlocksecRequest(ctx, request, "trace") } // profileHandler handles the profile tool requests func (s *Server) profileHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return s.handleBlocksecRequest(ctx, request, "profile") } // addressLabelHandler handles the address-label tool requests func (s *Server) addressLabelHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return s.handleBlocksecRequest(ctx, request, "address-label") } // balanceChangeHandler handles the balance-change tool requests func (s *Server) balanceChangeHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return s.handleBlocksecRequest(ctx, request, "balance-change") } // stateChangeHandler handles the state-change tool requests func (s *Server) stateChangeHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return s.handleBlocksecRequest(ctx, request, "state-change") } // Result represents a single data source result with success/error status type Result struct { Name string `json:"name"` Success bool `json:"success"` Data json.RawMessage `json:"data,omitempty"` Error string `json:"error,omitempty"` } // OverviewResult represents the combined result from all data sources type OverviewResult struct { Results map[string]Result `json:"results"` } // ChainData represents the data for a blockchain from chainlist.org type ChainData struct { Name string `json:"name"` Chain string `json:"chain"` ChainSlug string `json:"chainSlug"` ChainId uint64 `json:"chainId"` } // transactionOverviewHandler handles transaction-overview requests by calling all other handlers in parallel func (s *Server) transactionOverviewHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Define the endpoints to query endpoints := []struct { name string endpoint string }{ {"trace", "trace"}, {"profile", "profile"}, {"address_label", "address-label"}, {"balance_change", "balance-change"}, {"state_change", "state-change"}, } // Extract and validate parameters once chainId, txHash, err := extractRequestParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Create HTTP client client, err := createHTTPClient() if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Fetch cookies once if err := fetchBlocksecCookies(client); err != nil { return mcp.NewToolResultError(err.Error()), nil } // Create a wait group to synchronize goroutines var wg sync.WaitGroup // Create a mutex to protect the results map var mu sync.Mutex // Create the results map overviewResult := OverviewResult{ Results: make(map[string]Result), } // Process each endpoint in parallel for _, e := range endpoints { wg.Add(1) // Create a closure to capture the current endpoint go func(name, endpoint string) { defer wg.Done() // Call the API respBody, err := callBlocksecAPI(client, endpoint, chainId, txHash) // Store the result mu.Lock() defer mu.Unlock() if err != nil { overviewResult.Results[name] = Result{ Name: name, Success: false, Error: err.Error(), } return } // Store the successful result overviewResult.Results[name] = Result{ Name: name, Success: true, Data: respBody, } }(e.name, e.endpoint) } // Wait for all requests to complete wg.Wait() // Convert the overview result to JSON resultJSON, err := json.Marshal(overviewResult) if err != nil { return mcp.NewToolResultError(fmt.Sprintf("failed to marshal overview results: %v", err)), nil } return mcp.NewToolResultText(string(resultJSON)), nil } // fetchChainList fetches the chain list from chainlist.org with retries func fetchChainList() ([]ChainData, error) { maxRetries := 3 var lastErr error for attempt := 0; attempt <= maxRetries; attempt++ { resp, err := http.Get("https://chainlist.org/rpcs.json") if err != nil { lastErr = fmt.Errorf("failed to fetch chain list: %v", err) continue } // Read body and close response body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastErr = fmt.Errorf("failed to read response body: %v", err) continue } // Check status code if resp.StatusCode != http.StatusOK { lastErr = fmt.Errorf("chainlist.org returned non-200 status code: %d", resp.StatusCode) continue } // Try to unmarshal the response var chains []ChainData if err := json.Unmarshal(body, &chains); err != nil { lastErr = fmt.Errorf("failed to unmarshal chain list: %v", err) continue } // Success - return the chains return chains, nil } // If we've tried maxRetries times and still failed, return the last error return nil, fmt.Errorf("failed to fetch chain list after %d attempts: %v", maxRetries, lastErr) } // findChainByName searches for a chain by name, chain, or chainSlug func findChainByName(chains []ChainData, searchTerm string) (string, error) { searchTerm = strings.ToLower(strings.TrimSpace(searchTerm)) if searchTerm == "" { return "", fmt.Errorf("search term cannot be empty") } // Track closest matches var nameMatches []ChainData var containsMatches []ChainData // First pass: look for exact matches or contains for _, chain := range chains { nameLower := strings.ToLower(chain.Name) chainLower := strings.ToLower(chain.Chain) slugLower := strings.ToLower(chain.ChainSlug) // Check for exact matches first (prioritize these) if nameLower == searchTerm || chainLower == searchTerm || slugLower == searchTerm { nameMatches = append(nameMatches, chain) } else if strings.Contains(nameLower, searchTerm) { // If not exact, check if name contains the search term containsMatches = append(containsMatches, chain) } } // Return first exact match if found if len(nameMatches) > 0 { return strconv.FormatUint(nameMatches[0].ChainId, 10), nil } // Return first contains match if found if len(containsMatches) > 0 { return strconv.FormatUint(containsMatches[0].ChainId, 10), nil } return "", fmt.Errorf("no chain found matching '%s'", searchTerm) } // getChainIdByNameHandler handles requests to get a chain ID by name func (s *Server) getChainIdByNameHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract the chain name parameter chainName, err := request.RequireString("name") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("name is required: %v", err)), nil } // Fetch the chain list chains, err := fetchChainList() if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Find the chain by name chainId, err := findChainByName(chains, chainName) if err != nil { return mcp.NewToolResultError(err.Error()), nil } // Return the chain ID return mcp.NewToolResultText(chainId), nil }

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/mark3labs/phalcon-mcp'

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