1+ from mcp .server .fastmcp import FastMCP
2+ import subprocess
3+ from typing import Dict , List , Optional , Any
4+ import logging
5+ import sys
6+ import os
7+ import re
8+ from github import Github
9+
10+ # Create FastMCP instance
11+ mcp = FastMCP ("azure-sdk-python-mcp" )
12+
13+ # Setup logging
14+ logger = logging .getLogger ()
15+ logger .setLevel (logging .INFO )
16+ handler = logging .StreamHandler (sys .stderr )
17+ logger .addHandler (handler )
18+
19+ # Log environment information
20+ logger .info (f"Running with Python executable: { sys .executable } " )
21+ logger .info (f"Virtual environment path: { os .environ .get ('VIRTUAL_ENV' , 'Not running in a virtual environment' )} " )
22+ logger .info (f"Working directory: { os .getcwd ()} " )
23+
24+ def get_latest_commit (tspurl : str ) -> str :
25+ """Get the latest commit hash for a given TypeSpec config URL.
26+
27+ Args:
28+ tspurl: The URL to the tspconfig.yaml file.
29+
30+ Returns:
31+ The URL with the latest commit hash for the specified TypeSpec configuration.
32+ """
33+ try :
34+ # Extract URL components using regex
35+ res = re .match (
36+ r"^https://(?P<urlRoot>github|raw.githubusercontent).com/(?P<repo>[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?P<commit>[0-9a-f]{40}|[^/]+)/(?P<path>.*)/tspconfig.yaml$" ,
37+ tspurl
38+ )
39+
40+ if res is None :
41+ raise ValueError (f"Invalid TypeSpec URL format: { tspurl } " )
42+
43+ groups = res .groupdict ()
44+ commit = groups ["commit" ]
45+
46+ # Parse repository information
47+ repo_parts = tspurl .split ("/" )
48+ repo_name = f"{ repo_parts [3 ]} /{ repo_parts [4 ]} "
49+
50+ # Get path within repo
51+ parts = tspurl .split ("azure-rest-api-specs/blob/" )[1 ].split ("/" )
52+ parts .pop (0 ) # Remove branch name
53+ folder_path = "/" .join (parts )
54+
55+ # If commit is a branch name (not a SHA), get latest commit
56+ if not commit or commit == "main" or len (commit ) != 40 :
57+ g = Github ()
58+ repo = g .get_repo (repo_name )
59+ commits = repo .get_commits (path = folder_path )
60+
61+ if not commits :
62+ raise ValueError (f"No commits found for path: { folder_path } " )
63+
64+ latest_commit = commits [0 ].sha
65+ return f"https://raw.githubusercontent.com/{ groups ['repo' ]} /{ latest_commit } /{ groups ['path' ]} /tspconfig.yaml"
66+
67+ return f"https://raw.githubusercontent.com/{ groups ['repo' ]} /{ commit } /{ groups ['path' ]} /tspconfig.yaml"
68+
69+ except Exception as e :
70+ logger .error (f"Error getting latest commit: { str (e )} " )
71+ raise
72+
73+ def run_command (command : List [str ], cwd : str , is_typespec : bool = False ,
74+ typespec_args : Optional [Dict [str , Any ]] = None ) -> Dict [str , Any ]:
75+ """Run a command and return the result.
76+
77+ Args:
78+ command: The command to run as a list of strings
79+ cwd: Optional working directory to run the command in
80+ is_typespec: Whether this is a TypeSpec CLI command
81+ typespec_args: Optional arguments for TypeSpec commands
82+
83+ Returns:
84+ Dictionary with command execution results
85+ """
86+ # Handle TypeSpec commands
87+ if is_typespec :
88+ # Build TypeSpec CLI command
89+ if os .name == "nt" : # Windows
90+ cli_cmd = ["cmd.exe" , "/C" , "npx" , "@azure-tools/typespec-client-generator-cli" ] + command
91+ else : # Unix/Linux/MacOS
92+ cli_cmd = ["npx" , "@azure-tools/typespec-client-generator-cli" ] + command
93+
94+ # Add TypeSpec arguments
95+ if typespec_args :
96+ for key , value in typespec_args .items ():
97+ cli_cmd .append (f"--{ key } " )
98+ cli_cmd .append (str (value ))
99+
100+ command = cli_cmd
101+ # Log the command
102+ logger .info (f"Running command: { ' ' .join (command )} " )
103+
104+ try :
105+ logger .info (f"Using repository root as working directory: { cwd } " )
106+
107+ # Handle Windows command prefix if not a TypeSpec command
108+ if os .name == "nt" and not is_typespec :
109+ command = ["cmd.exe" , "/C" ] + command
110+
111+ # Execute command
112+ result = subprocess .run (
113+ command ,
114+ capture_output = True ,
115+ text = True ,
116+ cwd = cwd ,
117+ stdin = subprocess .DEVNULL ,
118+ )
119+
120+ if result .stdout :
121+ logger .info (f"Command output excerpt: { result .stdout [:100 ]} ..." )
122+
123+ return {
124+ "success" : result .returncode == 0 ,
125+ "stdout" : result .stdout ,
126+ "stderr" : result .stderr ,
127+ "code" : result .returncode
128+ }
129+ except Exception as e :
130+ logger .error (f"Error running command: { e } " )
131+ return {
132+ "success" : False ,
133+ "stdout" : "" ,
134+ "stderr" : str (e ),
135+ "code" : 1 ,
136+ }
137+
138+ @mcp .tool ("verify_setup" )
139+ def verify_setup_tool (command_path : str , tox_ini_path : str ) -> Dict [str , Any ]:
140+ """Verify machine is set up correctly for development.
141+
142+ :param str command_path: Path to the command in. (i.e. ./azure-sdk-for-python/)
143+ :param str tox_ini_path: Path to the tox.ini file. (i.e. ./azure-sdk-for-python/eng/tox/tox.ini)
144+ """
145+ def verify_installation (command : List [str ], name : str ) -> Dict [str , Any ]:
146+ """Helper function to verify installation of a tool."""
147+ logger .info (f"Checking installation of { name } " )
148+ result = run_command (command , cwd = command_path )
149+
150+ if not result ["success" ]:
151+ return {
152+ "success" : False ,
153+ "message" : f"{ name } is not installed or not available in PATH." ,
154+ "details" : {
155+ "stdout" : result ["stdout" ],
156+ "stderr" : result ["stderr" ],
157+ "exit_code" : result ["code" ]
158+ }
159+ }
160+
161+ version_output = result ["stdout" ].strip () or "No version output"
162+ return {
163+ "success" : True ,
164+ "message" : f"{ name } is installed. Version: { version_output } "
165+ }
166+
167+ # Verify required tools
168+ results = {
169+ "node" : verify_installation (["node" , "--version" ], "Node.js" ),
170+ "python" : verify_installation (["python" , "--version" ], "Python" ),
171+ "tox" : verify_installation (["tox" , "--version" , "-c" , tox_ini_path ], "tox" )
172+ }
173+
174+ return results
175+
176+ @mcp .tool ("validation_tool" )
177+ def tox_tool (package_path : str , environment : str , repo_path : str , tox_ini_path : str ) -> Dict [str , Any ]:
178+ """Run validation steps on a Python package using tox.
179+
180+ Args:
181+ package_path: Path to the Python package to test
182+ environment: tox environment to run (e.g., 'pylint', 'mypy')
183+ repo_path: Path to the repository root (i.e. ./azure-sdk-for-python/)
184+ tox_ini_path: Path to the tox.ini file (i.e. ./azure-sdk-for-python/eng/tox/tox.ini)
185+ """
186+ # Build and run tox command
187+ command = ["tox" , "run" , "-e" , environment , "-c" , tox_ini_path , "--root" , package_path ]
188+ return run_command (command , cwd = repo_path )
189+
190+ @mcp .tool ("init" )
191+ def init_tool (tsp_config_url : str , repo_path : str ) -> Dict [str , Any ]:
192+ """Initializes and generates a typespec client library directory given the url.
193+
194+ Args:
195+ tsp_config_url: The URL to the tspconfig.yaml file.
196+ repo_path: The path to the repository root (i.e. ./azure-sdk-for-python/).
197+ Returns:
198+ A dictionary containing the result of the command.
199+ """
200+ try :
201+ # Get updated URL with latest commit hash
202+ updated_url = get_latest_commit (tsp_config_url )
203+
204+ # Run the init command using the combined function
205+ return run_command (["init" ], cwd = repo_path , is_typespec = True ,
206+ typespec_args = {"tsp-config" : updated_url })
207+
208+ except RuntimeError as e :
209+ return {
210+ "success" : False ,
211+ "message" : str (e ),
212+ "stdout" : "" ,
213+ "stderr" : "" ,
214+ "code" : 1
215+ }
216+
217+ @mcp .tool ("init_local" )
218+ def init_local_tool (tsp_config_path : str , repo_path :str ) -> Dict [str , Any ]:
219+ """Initializes and subsequently generates a typespec client library directory from a local azure-rest-api-specs repo.
220+
221+ This command is used to generate a client library from a local azure-rest-api-specs repository. No additional
222+ commands are needed to generate the client library.
223+
224+ Args:
225+ tsp_config_path: The path to the local tspconfig.yaml file.
226+ repo_path: The path to the repository root (i.e. ./azure-sdk-for-python/).
227+
228+ Returns:
229+ A dictionary containing the result of the command. """
230+ try :
231+
232+ # Run the init command with local path using the combined function
233+ return run_command (["init" ], cwd = repo_path , is_typespec = True ,
234+ typespec_args = {"tsp-config" : tsp_config_path })
235+
236+ except RuntimeError as e :
237+ return {
238+ "success" : False ,
239+ "message" : str (e ),
240+ "stdout" : "" ,
241+ "stderr" : "" ,
242+ "code" : 1
243+ }
244+
245+ # Run the MCP server
246+ if __name__ == "__main__" :
247+ mcp .run (transport = 'stdio' )
0 commit comments