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