1+ #!/usr/bin/env python3
2+ """
3+ Sample Client for CodeBadger Toolkit Server
4+
5+ This client demonstrates basic usage of the CodeBadger Toolkit MCP server:
6+ 1. Generate a CPG for a local codebase
7+ 2. List methods in the codebase
8+ 3. Run a simple CPGQL query
9+ """
10+
11+ import asyncio
12+ import logging
13+ import sys
14+ import os
15+
16+ # Configure logging
17+ logging .basicConfig (level = logging .INFO , format = '%(asctime)s - %(levelname)s - %(message)s' )
18+ logger = logging .getLogger (__name__ )
19+
20+ try :
21+ from fastmcp import Client
22+ except ImportError :
23+ logger .error ("FastMCP not found. Install with: pip install fastmcp" )
24+ sys .exit (1 )
25+
26+
27+ def extract_tool_result (result ):
28+ """Extract dictionary data from CallToolResult"""
29+ if hasattr (result , 'content' ) and result .content :
30+ content_text = result .content [0 ].text
31+ try :
32+ import json
33+ parsed = json .loads (content_text )
34+
35+ # Handle complex results that return Scala output with embedded JSON
36+ if isinstance (parsed , dict ) and 'value' in parsed :
37+ value = parsed ['value' ]
38+ if isinstance (value , str ):
39+ # Look for embedded JSON in the Scala output
40+ import re
41+ # Match the escaped JSON string between quotes
42+ json_match = re .search (r'val res\d+: String = ("\{.*\}")' , value )
43+ if json_match :
44+ try :
45+ # Extract the escaped JSON string and unescape it
46+ escaped_json = json_match .group (1 )
47+ # Remove the surrounding quotes and unescape
48+ json_str = escaped_json [1 :- 1 ] # Remove quotes
49+ json_str = json_str .replace ('\\ "' , '"' ) # Unescape quotes
50+ json_str = json_str .replace ('\\ \\ ' , '\\ ' ) # Unescape backslashes
51+ return json .loads (json_str )
52+ except json .JSONDecodeError :
53+ pass
54+ # If no embedded JSON found, return the original parsed result
55+ return parsed
56+ else :
57+ return parsed
58+ else :
59+ return parsed
60+ except json .JSONDecodeError :
61+ return {"error" : content_text }
62+ return {}
63+
64+
65+ async def main ():
66+ """Main client function"""
67+ # Server URL - adjust if running on different host/port
68+ server_url = "http://localhost:4242/mcp"
69+
70+ # Path to the codebase - use container path since server runs in Docker
71+ # Host path: playground/codebases/core -> Container path: /app/playground/codebases/core
72+ codebase_path = "/app/playground/codebases/core"
73+
74+ logger .info ("=" * 60 )
75+ logger .info ("CODEBADGER TOOLKIT SAMPLE CLIENT" )
76+ logger .info ("=" * 60 )
77+ logger .info (f"Server URL: { server_url } " )
78+ logger .info (f"Codebase: { codebase_path } " )
79+
80+ try :
81+ async with Client (server_url ) as client :
82+ logger .info ("\n [1] Testing server connectivity..." )
83+ await client .ping ()
84+ logger .info ("✓ Server is responding" )
85+
86+ # ===== GENERATE CPG =====
87+ logger .info ("\n [2] Generating CPG for codebase..." )
88+ cpg_result = await client .call_tool ("generate_cpg" , {
89+ "source_type" : "local" ,
90+ "source_path" : codebase_path ,
91+ "language" : "c"
92+ })
93+
94+ cpg_dict = extract_tool_result (cpg_result )
95+ logger .info (f"CPG generation result: { cpg_dict } " )
96+
97+ if "codebase_hash" not in cpg_dict :
98+ logger .error ("❌ No codebase_hash returned" )
99+ return
100+
101+ codebase_hash = cpg_dict ["codebase_hash" ]
102+ logger .info (f"✓ CPG generation initiated. Hash: { codebase_hash } " )
103+
104+ # ===== WAIT FOR CPG TO BE READY =====
105+ logger .info ("\n [3] Waiting for CPG to be ready..." )
106+ cpg_ready = False
107+ max_attempts = 30
108+
109+ for attempt in range (max_attempts ):
110+ await asyncio .sleep (2 ) # Wait 2 seconds between checks
111+
112+ status_result = await client .call_tool ("get_cpg_status" , {
113+ "codebase_hash" : codebase_hash
114+ })
115+
116+ status_dict = extract_tool_result (status_result )
117+ status = status_dict .get ("status" )
118+ exists = status_dict .get ("exists" , False )
119+
120+ logger .info (f" Attempt { attempt + 1 } /{ max_attempts } : status={ status } , exists={ exists } " )
121+
122+ if status in ["ready" , "cached" ] and exists :
123+ cpg_ready = True
124+ logger .info ("✓ CPG is ready!" )
125+ break
126+
127+ if not cpg_ready :
128+ logger .error ("❌ CPG not ready after waiting" )
129+ return
130+
131+ # ===== LIST METHODS =====
132+ logger .info ("\n [4] Listing methods in the codebase..." )
133+ methods_result = await client .call_tool ("list_methods" , {
134+ "codebase_hash" : codebase_hash ,
135+ "limit" : 20
136+ })
137+
138+ methods_dict = extract_tool_result (methods_result )
139+
140+ if methods_dict .get ("success" ):
141+ methods = methods_dict .get ("methods" , [])
142+ total = methods_dict .get ("total" , 0 )
143+ logger .info (f"✓ Found { total } methods total, showing up to 20:" )
144+
145+ for method in methods [:10 ]: # Show first 10
146+ logger .info (f" - { method .get ('name' , 'unknown' )} in { method .get ('filename' , 'unknown' )} " )
147+
148+ if len (methods ) > 10 :
149+ logger .info (f" ... and { len (methods ) - 10 } more methods" )
150+ else :
151+ logger .error (f"❌ Failed to list methods: { methods_dict } " )
152+
153+ # ===== RUN SIMPLE QUERY - GET CODEBASE SUMMARY =====
154+ logger .info ("\n [5] Getting codebase summary..." )
155+
156+ summary_result = await client .call_tool ("get_codebase_summary" , {
157+ "codebase_hash" : codebase_hash
158+ })
159+
160+ summary_dict = extract_tool_result (summary_result )
161+
162+ if summary_dict .get ("success" ):
163+ summary = summary_dict .get ("summary" , {})
164+ logger .info ("✓ Codebase summary retrieved:" )
165+ logger .info (f" Language: { summary .get ('language' )} " )
166+ logger .info (f" Files: { summary .get ('total_files' )} " )
167+ logger .info (f" Methods: { summary .get ('total_methods' )} " )
168+ logger .info (f" Calls: { summary .get ('total_calls' )} " )
169+ else :
170+ logger .error (f"❌ Failed to get summary: { summary_dict } " )
171+
172+ # ===== RUN ANOTHER QUERY - LIST CALLS =====
173+ logger .info ("\n [6] Listing function calls..." )
174+
175+ calls_result = await client .call_tool ("list_calls" , {
176+ "codebase_hash" : codebase_hash ,
177+ "limit" : 10
178+ })
179+
180+ calls_dict = extract_tool_result (calls_result )
181+
182+ if calls_dict .get ("success" ):
183+ calls = calls_dict .get ("calls" , [])
184+ total = calls_dict .get ("total" , 0 )
185+ logger .info (f"✓ Found { total } calls total, showing up to 10:" )
186+
187+ for call in calls [:5 ]: # Show first 5
188+ logger .info (f" - { call .get ('caller' , 'unknown' )} -> { call .get ('callee' , 'unknown' )} " )
189+
190+ if len (calls ) > 5 :
191+ logger .info (f" ... and { len (calls ) - 5 } more calls" )
192+ else :
193+ logger .error (f"❌ Failed to list calls: { calls_dict } " )
194+
195+ logger .info ("\n " + "=" * 60 )
196+ logger .info ("SAMPLE CLIENT COMPLETED SUCCESSFULLY!" )
197+ logger .info ("=" * 60 )
198+
199+ except Exception as e :
200+ logger .error (f"❌ Client error: { e } " , exc_info = True )
201+ sys .exit (1 )
202+
203+
204+ if __name__ == "__main__" :
205+ try :
206+ asyncio .run (main ())
207+ except KeyboardInterrupt :
208+ logger .info ("\n 🛑 Client interrupted by user" )
209+ sys .exit (1 )
0 commit comments