33
44module Echidna.MCP where
55
6+ import Control.Concurrent (forkIO )
7+ import Control.Monad (forever , when )
68import Control.Concurrent.STM
7- import Data.IORef (readIORef , IORef )
9+ import Data.IORef (readIORef , writeIORef , modifyIORef' , newIORef , IORef )
810import Data.List (find , isPrefixOf )
911import qualified Data.Maybe
1012import qualified Data.Set as Set
1113import Data.Text (Text , pack , unpack )
1214import qualified Data.Text as T
15+ import Data.Time (UTCTime , getCurrentTime , diffUTCTime )
1316import Text.Printf (printf )
1417import qualified Data.Map as Map
1518import Text.Read (readMaybe )
@@ -22,14 +25,20 @@ import EVM.Solidity (SolcContract(..))
2225import EVM.Types (Addr )
2326import EVM.ABI (AbiValue (.. ))
2427import Echidna.Types.Tx (Tx (.. ), TxCall (.. ))
25- import Echidna.Types.Coverage (CoverageFileType (.. ), mergeCoverageMaps )
28+ import Echidna.Types.Coverage (CoverageFileType (.. ), mergeCoverageMaps , coverageStats )
2629import Echidna.Output.Source (ppCoveredCode , saveLcovHook )
2730
2831import Echidna.Types.Config (Env (.. ), EConfig (.. ))
29- import Echidna.Types.Campaign (getNFuzzWorkers , CampaignConf (.. ))
30- import Echidna.Types.InterWorker (Bus , Message (.. ), WrappedMessage (.. ), AgentId (.. ), FuzzerCmd (.. ))
32+ import Echidna.Types.Campaign (getNFuzzWorkers , CampaignConf (.. ), WorkerState ( .. ) )
33+ import Echidna.Types.InterWorker (Bus , Message (.. ), WrappedMessage (.. ), AgentId (.. ), FuzzerCmd (.. ), BroadcastMsg ( .. ) )
3134import Echidna.Pretty (ppTx )
3235
36+ -- | Status state to track coverage info
37+ data StatusState = StatusState
38+ { lastCoverageTime :: Maybe UTCTime
39+ , coveredFunctions :: [Text ]
40+ }
41+
3342-- | MCP Tool Definition
3443-- Simulates the definition of a tool exposed by an MCP server.
3544type ToolExecution = [(Text , Text )] -> Env -> Bus -> IORef [Text ] -> IO String
@@ -40,11 +49,37 @@ data Tool = Tool
4049 , execute :: ToolExecution
4150 }
4251
43- -- | Implementation of read_corpus tool
44- readCorpusTool :: ToolExecution
45- readCorpusTool _ env _ _ = do
52+ -- | Helper to get function name from Tx
53+ getFunctionName :: Tx -> Text
54+ getFunctionName tx = case tx. call of
55+ SolCall (name, _) -> name
56+ _ -> " unknown"
57+
58+ -- | Implementation of status tool
59+ statusTool :: [IORef WorkerState ] -> IORef StatusState -> ToolExecution
60+ statusTool workerRefs statusRef _ env _ _ = do
4661 c <- readIORef env. corpusRef
47- return $ printf " Corpus Size: %d" (Set. size c)
62+ st <- readIORef statusRef
63+ now <- getCurrentTime
64+
65+ -- Iterations
66+ workers <- mapM readIORef workerRefs
67+ let iterations = sum $ map (. ncalls) workers
68+ let maxIterations = env. cfg. campaignConf. testLimit
69+
70+ -- Coverage
71+ (covPoints, _) <- coverageStats env. coverageRefInit env. coverageRefRuntime
72+
73+ let timeStr = case st. lastCoverageTime of
74+ Nothing -> " Never"
75+ Just t -> show (round $ diffUTCTime now t)
76+
77+ funcs = if null st. coveredFunctions
78+ then " None"
79+ else unpack $ T. intercalate " \n - " st. coveredFunctions
80+
81+ return $ printf " Corpus Size: %d\n Iterations: %d/%d\n Coverage: %d\n Time since last coverage: %s\n Last 10 covered functions:\n - %s"
82+ (Set. size c) iterations maxIterations covPoints timeStr funcs
4883
4984-- | Implementation of inspect_corpus_transactions tool
5085inspectCorpusTransactionsTool :: ToolExecution
@@ -252,21 +287,42 @@ showCoverageTool args env _ _ = do
252287 candidates -> return $ printf " Error: Ambiguous contract name '%s'. Found: %s" (unpack contractName) (unpack $ T. intercalate " , " $ map fst candidates)
253288
254289-- | Registry of available tools
255- availableTools :: [Tool ]
256- availableTools =
257- [ Tool " read_corpus " " Read the current corpus size " readCorpusTool
290+ availableTools :: [IORef WorkerState ] -> IORef StatusState -> [ Tool ]
291+ availableTools workerRefs statusRef =
292+ [ Tool " status " " Show fuzzing campaign status " (statusTool workerRefs statusRef)
258293 , Tool " inspect_corpus_transactions" " Browse the corpus transactions" inspectCorpusTransactionsTool
259294 , Tool " inject_transaction" " Inject a transaction into a sequence and execute it" injectTransactionTool
260295 , Tool " dump_lcov" " Dump coverage in LCOV format" dumpLcovTool
261296 , Tool " fuzz_transaction" " Fuzz a transaction with optional concrete arguments" fuzzTransactionTool
262297 , Tool " clear_priorities" " Clear the function prioritization list" clearPrioritiesTool
263- , Tool " read_logs" " Read the last 100 log messages" readLogsTool
298+ -- , Tool "read_logs" "Read the last 100 log messages" readLogsTool
264299 , Tool " show_coverage" " Show coverage report for a particular contract" showCoverageTool
265300 ]
266301
267302-- | Run the MCP Server
268- runMCPServer :: Env -> Int -> IORef [Text ] -> IO ()
269- runMCPServer env port logsRef = do
303+ runMCPServer :: Env -> [IORef WorkerState ] -> Int -> IORef [Text ] -> IO ()
304+ runMCPServer env workerRefs port logsRef = do
305+ statusRef <- newIORef (StatusState Nothing [] )
306+
307+ -- Spawn listener for coverage events
308+ myBus <- atomically $ dupTChan env. bus
309+ _ <- forkIO $ forever $ do
310+ msg <- atomically $ readTChan myBus
311+ case msg of
312+ WrappedMessage _ (Broadcast (NewCoverageInfo _ txs isReplaying)) -> do
313+ when (not isReplaying) $ do
314+ now <- getCurrentTime
315+ let funcNames = map getFunctionName txs
316+ lastFunc = if null funcNames then " unknown" else last funcNames
317+
318+ modifyIORef' statusRef $ \ st -> st
319+ { lastCoverageTime = Just now
320+ , coveredFunctions = take 10 (lastFunc : st. coveredFunctions)
321+ }
322+ _ -> return ()
323+
324+ let toolsList = availableTools workerRefs statusRef
325+
270326 let httpConfig = HttpConfig
271327 { httpPort = port
272328 , httpHost = " 127.0.0.1"
@@ -277,7 +333,7 @@ runMCPServer env port logsRef = do
277333 let serverInfo = McpServerInfo
278334 { serverName = " Echidna MCP Server"
279335 , serverVersion = " 1.0.0"
280- , serverInstructions = " Echidna Agent Interface. Available tools: read_corpus , inspect_corpus_transactions, dump_lcov, fuzz_transaction, clear_priorities, read_logs, show_coverage"
336+ , serverInstructions = " Echidna Agent Interface. Available tools: status , inspect_corpus_transactions, dump_lcov, fuzz_transaction, clear_priorities, read_logs, show_coverage"
281337 }
282338
283339 let mkToolDefinition :: Tool -> ToolDefinition
@@ -317,6 +373,10 @@ runMCPServer env port logsRef = do
317373 { properties = []
318374 , required = []
319375 }
376+ " status" -> InputSchemaDefinitionObject
377+ { properties = []
378+ , required = []
379+ }
320380 _ -> InputSchemaDefinitionObject
321381 { properties = []
322382 , required = []
@@ -325,11 +385,11 @@ runMCPServer env port logsRef = do
325385 , toolDefinitionMeta = Nothing
326386 }
327387
328- let toolDefs = map mkToolDefinition availableTools
388+ let toolDefs = map mkToolDefinition toolsList
329389
330390 let handleToolCall :: ToolName -> [(ArgumentName , ArgumentValue )] -> IO (Either Error Content )
331391 handleToolCall name args = do
332- case find (\ t -> pack t. toolName == name) availableTools of
392+ case find (\ t -> pack t. toolName == name) toolsList of
333393 Nothing -> return $ Left $ UnknownTool name
334394 Just tool -> do
335395 result <- tool. execute args env env. bus logsRef
0 commit comments