SURIAPRAKASH1 commited on
Commit
9a1ed74
Β·
1 Parent(s): 5775dc6

FastMCP server embracess auto registery tools

Browse files
Files changed (1) hide show
  1. server.py +9 -312
server.py CHANGED
@@ -1,29 +1,15 @@
1
- import logging, os, json, sys
2
- from typing import Any, Literal, Optional
3
- from pathlib import Path
4
- import subprocess
5
- from dotenv import load_dotenv
6
  import argparse
7
- load_dotenv()
 
8
 
9
- # -------------
10
- # Logging: logging + stdio = βœ…, print + stdio = ❌
11
- # -------------
12
 
13
- logger = logging.getLogger(__name__)
14
- logger.setLevel(logging.DEBUG)
15
 
16
- fmt = "%(asctime)s -- %(levelname)s -- %(name)s -- %(message)s"
17
- file_handler = logging.FileHandler("multitools-server.log")
18
- file_handler.setFormatter(logging.Formatter(fmt))
19
-
20
- logger.addHandler(file_handler)
21
-
22
- # Now 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 😞.
23
  try:
24
  from mcp.server.fastmcp import FastMCP
25
- from bs4 import BeautifulSoup
26
- import httpx
27
  except ImportError as e:
28
  logger.error("Got Error when Importing Packages: \n%s", e)
29
  sys.exit(1)
@@ -36,299 +22,10 @@ except Exception as e:
36
  # -------------------------
37
  mcp = FastMCP("multitools-server")
38
 
39
- # --------------
40
- # Configuration
41
- #---------------
42
- BASE_CRICKET_URL = os.environ.get("BASE_CRICKET_URL")
43
- logger.warning("Env variable BASE_CRICKET_URL Not-Found may cause error...") if not BASE_CRICKET_URL else logger.info("")
44
-
45
- # PR template directory
46
- TEMPLATES_DIR = Path(__file__).parent / "templates"
47
- logger.warning("TEMPLATES_DIR Not-Found may cause Error...") if not TEMPLATES_DIR else logger.info("TEMPLATES_DIR: \n%s", TEMPLATES_DIR)
48
-
49
- # Default PR templates
50
- DEFAULT_TEMPLATES = {
51
- "bug.md": "Bug Fix",
52
- "feature.md": "Feature",
53
- "docs.md": "Documentation",
54
- "refactor.md": "Refactor",
55
- "test.md": "Test",
56
- "performance.md": "Performance",
57
- "security.md": "Security"
58
- }
59
-
60
- # Type mapping for PR templates
61
- TYPE_MAPPING = {
62
- "bug": "bug.md",
63
- "fix": "bug.md",
64
- "feature": "feature.md",
65
- "enhancement": "feature.md",
66
- "docs": "docs.md",
67
- "documentation": "docs.md",
68
- "refactor": "refactor.md",
69
- "cleanup": "refactor.md",
70
- "test": "test.md",
71
- "testing": "test.md",
72
- "performance": "performance.md",
73
- "optimization": "performance.md",
74
- "security": "security.md"
75
- }
76
-
77
-
78
- # ----------------------
79
- # Available tools|resources|prompts|sampling for LLM
80
- # -----------------------
81
-
82
- async def cricket_source(mode: str, want: str) -> str:
83
- """Fetches whole html from source url then extracts html container that contains necessary details"""
84
-
85
- if mode == "live":
86
- url = f"{BASE_CRICKET_URL}/cricket-match/live-scores"
87
- elif mode == 'upcomming':
88
- url = f"{BASE_CRICKET_URL}/cricket-match/live-scores/upcoming-matches"
89
- else:
90
- error = f"Not Implemented: Currently there's no implementation to handle {mode}. Only handels live, upcomming"
91
- return json.dumps({"error": error})
92
-
93
- try:
94
- async with httpx.AsyncClient(timeout= 10.0) as client:
95
- response = await client.get(url= url)
96
- response.raise_for_status() # if ain't 2xx it will raise HTTP error
97
- except httpx.HTTPError as e:
98
- return json.dumps({'error': str(e)})
99
- except Exception as e:
100
- return json.dumps({'error': str(e)})
101
-
102
- if response:
103
- # convert htmldoc content to proper html form using bs
104
- html = BeautifulSoup(response.content, "html.parser")
105
-
106
- # find where the content is
107
- container = html.find("div", class_= 'cb-col cb-col-100 cb-rank-tabs')
108
- if mode in ['live', 'upcomming'] and want == "text":
109
- text = container.get_text(separator=" ", strip= True)
110
- return json.dumps({"output": str(text)})
111
- elif mode == 'live' and want == 'herf':
112
- herfs_list = container.find_all("a", class_ = "cb-text-link cb-mtch-lnks")
113
- herfs_string = ",".join(str(tag) for tag in herfs_list)
114
- return json.dumps({"output": herfs_string})
115
- else:
116
- return json.dumps({"error": f"Not Implemented for {mode} with {want}"})
117
-
118
- else:
119
- return json.dumps({"error": "No Available details right now!"})
120
-
121
-
122
- @mcp.tool()
123
- async def fetch_cricket_details(mode: Literal["live", "upcomming"])-> str:
124
- """ Get cricket Live or Upcomming match details
125
- Args:
126
- mode : Either "live" or "upcomming"
127
- """
128
- response = await cricket_source(mode.strip().lower(), want= 'text')
129
- return response
130
-
131
-
132
- @mcp.tool()
133
- async def live_cricket_scorecard_herf()-> str:
134
- """String of comma separated anchor tags contains herf attributes that pointing to live cricket scorecards """
135
- response = await cricket_source('live', 'herf')
136
- return response
137
-
138
 
139
- @mcp.tool()
140
- async def live_cricket_scorecard(herf: str)-> str:
141
- """Live cricket match scorecard details for given herf.
142
- (e.g, herf = "/live-cricket-scorecard/119495/cd-vs-hbh-7th-match-global-super-league-2025")
143
-
144
- Args:
145
- herf: live cricket match scorecard endpoint
146
- """
147
- scorecard_url = f"{BASE_CRICKET_URL}{herf}"
148
-
149
- try:
150
- async with httpx.AsyncClient(timeout= 10.0) as client:
151
- response = await client.get(url = scorecard_url)
152
- response.raise_for_status()
153
- except httpx.HTTPError as e:
154
- return json.dumps({"error": str(e)})
155
- except Exception as 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=" ", 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
- @mcp.tool()
290
- async def get_pr_templates() -> str:
291
- """List available PR templates with their content."""
292
- templates = [
293
- {
294
- "filename": filename,
295
- "type": template_type,
296
- "content": (TEMPLATES_DIR / filename).read_text()
297
- }
298
- for filename, template_type in DEFAULT_TEMPLATES.items()
299
- ]
300
-
301
- return json.dumps(templates, indent=2)
302
-
303
-
304
- @mcp.tool()
305
- async def suggest_template(changes_summary: str, change_type: str) -> str:
306
- """Let LLM analyze the changes and suggest the most appropriate PR template.
307
-
308
- Args:
309
- changes_summary: Your analysis of what the changes do
310
- change_type: The type of change you've identified (bug, feature, docs, refactor, test, etc.)
311
- """
312
-
313
- # Get available templates
314
- templates_response = await get_pr_templates()
315
- templates = json.loads(templates_response)
316
-
317
- # Find matching template
318
- template_file = TYPE_MAPPING.get(change_type.lower(), "feature.md")
319
- selected_template = next(
320
- (t for t in templates if t["filename"] == template_file),
321
- templates[0] # Default to first template if no match
322
- )
323
-
324
- suggestion = {
325
- "recommended_template": selected_template,
326
- "reasoning": f"Based on your analysis: '{changes_summary}', this appears to be a {change_type} change.",
327
- "template_content": selected_template["content"],
328
- "usage_hint": "LLM can help you fill out this template based on the specific changes in your PR."
329
- }
330
-
331
- return json.dumps(suggestion, indent=2)
332
 
333
 
334
  if __name__ == "__main__":
 
1
+ import os, sys
 
 
 
 
2
  import argparse
3
+ from dotenv import load_dotenv
4
+ load_dotenv()
5
 
6
+ from logging_utils import get_logger
7
+ logger = get_logger(name = __name__)
 
8
 
 
 
9
 
 
 
 
 
 
 
 
10
  try:
11
  from mcp.server.fastmcp import FastMCP
12
+ from registry import get_tool_functions
 
13
  except ImportError as e:
14
  logger.error("Got Error when Importing Packages: \n%s", e)
15
  sys.exit(1)
 
22
  # -------------------------
23
  mcp = FastMCP("multitools-server")
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Auto register all tools
27
+ for name, func in get_tool_functions().items():
28
+ mcp.tool(name)(func)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
 
31
  if __name__ == "__main__":