SURIAPRAKASH1 commited on
Commit
2b61c4d
·
1 Parent(s): 917f42b

isolatated mcp primitives(tool) to provide dynamic FastMcp and gradio servers

Browse files
Files changed (1) hide show
  1. primitives/tools.py +318 -0
primitives/tools.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, sys
2
+ from typing import Literal
3
+ from pathlib import Path
4
+ import subprocess
5
+ from dotenv import load_dotenv
6
+ load_dotenv()
7
+
8
+ from logging_utils import get_logger
9
+ logger = get_logger(name = __name__)
10
+
11
+ # Import neccessary Packages. Sanity checks for libraries cause connecting mcp-client to mcp-server via stdio, client launches mcp-server as subprocess so client uses it's env libraries. If we use different env like in this case, client will invoke connection closed error and never gives any clue what's went wrong that's frustrating 😞.
12
+ try:
13
+ from bs4 import BeautifulSoup
14
+ import httpx
15
+ import gradio as gr
16
+ except ImportError as e:
17
+ logger.error("Got Error when Importing Packages: \n%s", e)
18
+ sys.exit(1)
19
+ except Exception as e:
20
+ logger.error("Got UnExcepted Error when Importing Packages: \n%s", e)
21
+ sys.exit(1)
22
+
23
+ # --------------
24
+ # Configuration
25
+ #---------------
26
+ BASE_CRICKET_URL = os.environ.get("BASE_CRICKET_URL")
27
+ logger.warning("Env variable BASE_CRICKET_URL Not-Found may cause error...") if not BASE_CRICKET_URL else logger.info("")
28
+
29
+ # PR template directory
30
+ TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
31
+ logger.warning("TEMPLATES_DIR Not-Found may cause Error...") if not TEMPLATES_DIR else logger.info("TEMPLATES_DIR: \n%s", TEMPLATES_DIR)
32
+
33
+ # Default PR templates
34
+ DEFAULT_TEMPLATES = {
35
+ "bug.md": "Bug Fix",
36
+ "feature.md": "Feature",
37
+ "docs.md": "Documentation",
38
+ "refactor.md": "Refactor",
39
+ "test.md": "Test",
40
+ "performance.md": "Performance",
41
+ "security.md": "Security"
42
+ }
43
+
44
+ # Type mapping for PR templates
45
+ TYPE_MAPPING = {
46
+ "bug": "bug.md",
47
+ "fix": "bug.md",
48
+ "feature": "feature.md",
49
+ "enhancement": "feature.md",
50
+ "docs": "docs.md",
51
+ "documentation": "docs.md",
52
+ "refactor": "refactor.md",
53
+ "cleanup": "refactor.md",
54
+ "test": "test.md",
55
+ "testing": "test.md",
56
+ "performance": "performance.md",
57
+ "optimization": "performance.md",
58
+ "security": "security.md"
59
+ }
60
+
61
+
62
+ # -----------------
63
+ # Available tools
64
+ # -----------------
65
+
66
+ async def cricket_source(
67
+ mode: Literal["live", "upcomming"], want: Literal["text", "herf"] ) -> str:
68
+ """Fetches whole html from source extracts html container that contains necessary details
69
+
70
+ Args:
71
+ mode: Which type of match do you wanna see details about.
72
+ want: Extractor name to get details from html container.
73
+ """
74
+
75
+ if mode == "live":
76
+ url = f"{BASE_CRICKET_URL}/cricket-match/live-scores"
77
+ elif mode == 'upcomming':
78
+ url = f"{BASE_CRICKET_URL}/cricket-match/live-scores/upcoming-matches"
79
+ else:
80
+ error = f"Not Implemented: Currently there's no implementation to handle {mode}. Only handels live, upcomming"
81
+ return json.dumps({"error": error})
82
+
83
+ try:
84
+ async with httpx.AsyncClient(timeout= 10.0) as client:
85
+ response = await client.get(url= url)
86
+ response.raise_for_status() # if ain't 2xx it will raise HTTP error
87
+ except httpx.HTTPError as e:
88
+ return json.dumps({'error': str(e)})
89
+ except Exception as e:
90
+ return json.dumps({'error': str(e)})
91
+
92
+ if response:
93
+ # convert htmldoc content to proper html form using bs
94
+ html = BeautifulSoup(response.content, "html.parser")
95
+
96
+ # find where the content is
97
+ container = html.find("div", class_= 'cb-col cb-col-100 cb-rank-tabs')
98
+ if mode in ['live', 'upcomming'] and want == "text":
99
+ text = container.get_text(separator=" ", strip= True)
100
+ return json.dumps({f"{mode} details": str(text)})
101
+ elif mode == 'live' and want == 'herf':
102
+ herfs_list = container.find_all("a", class_ = "cb-text-link cb-mtch-lnks")
103
+ herfs_string = ",".join(str(tag) for tag in herfs_list)
104
+ return json.dumps({"herfs_strings": herfs_string})
105
+ else:
106
+ return json.dumps({"error": f"Not Implemented for {mode} with {want}"})
107
+
108
+ else:
109
+ return json.dumps({"error": "No Available details right now!"})
110
+
111
+
112
+ async def fetch_cricket_details(mode: Literal["live", "upcomming"])-> str:
113
+ """Get cricket Live or Upcomming match details
114
+
115
+ Args:
116
+ mode : Either "live" or "upcomming"
117
+ """
118
+ response = await cricket_source(mode.strip().lower(), want= 'text')
119
+ return response
120
+
121
+
122
+ async def live_cricket_scorecard_herf()-> str:
123
+ """String of comma separated anchor tags with herf attributes that pointing to live cricket scorecards """
124
+ response = await cricket_source('live', 'herf')
125
+ return response
126
+
127
+
128
+ async def live_cricket_scorecard(herf: str)-> str:
129
+ """Get Live cricket match scorecard details.
130
+ (e.g, herf = "/live-cricket-scorecard/119495/cd-vs-hbh-7th-match-global-super-league-2025")
131
+
132
+ Args:
133
+ herf: live cricket match scorecard endpoint
134
+ """
135
+ scorecard_url = f"{BASE_CRICKET_URL}{herf}"
136
+
137
+ try:
138
+ async with httpx.AsyncClient(timeout= 10.0) as client:
139
+ response = await client.get(url = scorecard_url)
140
+ response.raise_for_status()
141
+ except httpx.HTTPError as e:
142
+ return json.dumps({"error": str(e)})
143
+ except Exception as e:
144
+ return json.dumps({'error': str(e)})
145
+
146
+ # extract html container
147
+ if response:
148
+ html = BeautifulSoup(response.content, "html.parser")
149
+ live_scorecard = html.find("div", timeout = "30000")
150
+ details = live_scorecard.get_text(separator=" ", strip=True)
151
+ return json.dumps({'output': str(details)})
152
+ else:
153
+ return json.dumps({'error': "No Available details right now"})
154
+
155
+
156
+ async def analyze_file_changes(
157
+ base_branch: str = "main",
158
+ include_diff: bool = True,
159
+ max_diff_lines: int = 400,
160
+ working_directory: str = "" # Optional[str] gradio will give error
161
+ ) -> str:
162
+ """Get the full diff and list of changed files in the current git repository.
163
+
164
+ Args:
165
+ base_branch: Base branch to compare against (default: main)
166
+ include_diff: Include the full diff content (default: true)
167
+ max_diff_lines: Maximum number of diff lines to include (default: 400)
168
+ working_directory: Directory to run git commands in (default: current directory)
169
+ """
170
+ try:
171
+
172
+ # Use provided working directory or current directory
173
+ cwd = working_directory if working_directory else os.getcwd()
174
+
175
+ # Debug output
176
+ debug_info = {
177
+ "provided_working_directory": working_directory,
178
+ "actual_cwd": cwd,
179
+ "server_process_cwd": os.getcwd(),
180
+ "server_file_location": str(Path(__file__).parent),
181
+ }
182
+
183
+ # Get list of changed files
184
+ files_result = subprocess.run(
185
+ ["git", "diff", "--name-status", f"{base_branch}...HEAD"],
186
+ capture_output=True,
187
+ text=True,
188
+ check=True,
189
+ cwd=cwd
190
+ )
191
+
192
+ # Get diff statistics
193
+ stat_result = subprocess.run(
194
+ ["git", "diff", "--stat", f"{base_branch}...HEAD"],
195
+ capture_output=True,
196
+ text=True,
197
+ cwd=cwd
198
+ )
199
+
200
+ # Get the actual diff if requested
201
+ diff_content = ""
202
+ truncated = False
203
+ if include_diff:
204
+ diff_result = subprocess.run(
205
+ ["git", "diff", f"{base_branch}...HEAD"],
206
+ capture_output=True,
207
+ text=True,
208
+ cwd=cwd
209
+ )
210
+ diff_lines = diff_result.stdout.split('\n')
211
+ # Check if we need to truncate
212
+ if len(diff_lines) > max_diff_lines:
213
+ diff_content = '\n'.join(diff_lines[:max_diff_lines])
214
+ diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
215
+ diff_content += "\n... Use max_diff_lines parameter to see more ..."
216
+ truncated = True
217
+ else:
218
+ diff_content = diff_result.stdout
219
+
220
+ # Get commit messages for context
221
+ commits_result = subprocess.run(
222
+ ["git", "log", "--oneline", f"{base_branch}..HEAD"],
223
+ capture_output=True,
224
+ text=True,
225
+ cwd=cwd
226
+ )
227
+
228
+ analysis = {
229
+ "base_branch": base_branch,
230
+ "files_changed": files_result.stdout,
231
+ "statistics": stat_result.stdout,
232
+ "commits": commits_result.stdout,
233
+ "diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
234
+ "truncated": truncated,
235
+ "total_diff_lines": len(diff_lines) if include_diff else 0,
236
+ "_debug": debug_info
237
+ }
238
+
239
+ return json.dumps(analysis, indent=2)
240
+
241
+ except subprocess.CalledProcessError as e:
242
+ return json.dumps({"error": f"Git error: {e.stderr}"})
243
+ except Exception as e:
244
+ return json.dumps({"error": str(e)})
245
+
246
+
247
+ async def get_pr_templates() -> str:
248
+ """List available PR templates with their content."""
249
+ templates = [
250
+ {
251
+ "filename": filename,
252
+ "type": template_type,
253
+ "content": (TEMPLATES_DIR / filename).read_text()
254
+ }
255
+ for filename, template_type in DEFAULT_TEMPLATES.items()
256
+ ]
257
+
258
+ return json.dumps(templates, indent=2)
259
+
260
+
261
+ async def suggest_template(changes_summary: str, change_type: str) -> str:
262
+ """Let LLM analyze the changes and suggest the most appropriate PR template.
263
+
264
+ Args:
265
+ changes_summary: Your analysis of what the changes do
266
+ change_type: The type of change you've identified (bug, feature, docs, refactor, test, etc.)
267
+ """
268
+
269
+ # Get available templates
270
+ templates_response = await get_pr_templates()
271
+ templates = json.loads(templates_response)
272
+
273
+ # Find matching template
274
+ template_file = TYPE_MAPPING.get(change_type.lower(), "feature.md")
275
+ selected_template = next(
276
+ (t for t in templates if t["filename"] == template_file),
277
+ templates[0] # Default to first template if no match
278
+ )
279
+
280
+ suggestion = {
281
+ "recommended_template": selected_template,
282
+ "reasoning": f"Based on your analysis: '{changes_summary}', this appears to be a {change_type} change.",
283
+ "template_content": selected_template["content"],
284
+ "usage_hint": "LLM can help you fill out this template based on the specific changes in your PR."
285
+ }
286
+
287
+ return json.dumps(suggestion, indent=2)
288
+
289
+ # Metadata for Gradio components
290
+ TOOL_COMPONENTS = {
291
+ "cricket_source": {
292
+ "is_gradio_api": True,
293
+ },
294
+ "fetch_cricket_details": {
295
+ "is_gradio_api": False,
296
+ "inputs": gr.Radio(["live", "upcomming"], label="Mode"),
297
+ "outputs": gr.JSON(label= "Details"),
298
+ },
299
+ "live_cricket_scorecard_herf": {
300
+ "is_gradio_api": False,
301
+ "inputs": None,
302
+ "outputs": gr.JSON(label = 'Scorecard Herfs')
303
+ },
304
+ "live_cricket_scorecard": {
305
+ "is_gradio_api": True,
306
+ },
307
+ "analyze_file_changes": {
308
+ "is_gradio_api": True,
309
+ },
310
+ "get_pr_templates": {
311
+ "is_gradio_api": False,
312
+ "inputs": None, # if None gradio will automatically create Generate button for us
313
+ "outputs": gr.JSON(label= "Available PR templates")
314
+ },
315
+ "suggest_template": {
316
+ "is_gradio_api": True
317
+ }
318
+ }