SURIAPRAKASH1 commited on
Commit
6b1354b
Β·
1 Parent(s): 13449ed

full server -> setup

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