From b60bb9a3dd44be8ed864e892627f53f65e870bba Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 08:33:34 -0700 Subject: [PATCH 1/7] thjis --- tools/mcp/typespec-story/.python-version | 1 - tools/mcp/typespec-story/main.py | 362 +++++++++++++---------- tools/mcp/typespec-story/pyproject.toml | 3 +- 3 files changed, 211 insertions(+), 155 deletions(-) delete mode 100644 tools/mcp/typespec-story/.python-version diff --git a/tools/mcp/typespec-story/.python-version b/tools/mcp/typespec-story/.python-version deleted file mode 100644 index e4fba2183587..000000000000 --- a/tools/mcp/typespec-story/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/tools/mcp/typespec-story/main.py b/tools/mcp/typespec-story/main.py index 918641b23a59..30a37195d4ac 100644 --- a/tools/mcp/typespec-story/main.py +++ b/tools/mcp/typespec-story/main.py @@ -3,6 +3,7 @@ from typing import Dict, Optional, Any import logging import sys +from github import Github logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -10,7 +11,57 @@ logger.addHandler(hander) # Initialize FastMCP server -mcp = FastMCP("typespec-story") +mcp = FastMCP("typespec") + +def get_latest_commit(package_name: str) -> str: + """Get the URL to the tspconfig.yaml file for a given package name from the GitHub repository. + + Args: + package_name: The name of the package to get the latest commit for, e.g. "azure-eventgrid". + + Returns: + The URL to the tspconfig.yaml file for the package in the Azure/azure-rest-api-specs repository. + """ + # Extract the service name from the package name + # For example, extract "eventgrid" from "azure-eventgrid" + if package_name.startswith("azure-"): + service_name = package_name[len("azure-"):] + else: + service_name = package_name + + # Define the folder path in the repo + folder_path = f"specification/{service_name}" + logger.info(f"Looking for commits in folder: {folder_path}") + + try: + g = Github() + repo = g.get_repo("Azure/azure-rest-api-specs") + + # Get commits that affect the specific folder + commits = repo.get_commits(path=folder_path) + + if commits.totalCount == 0: + logger.warning(f"No commits found for path: {folder_path}") + # Fall back to main branch + tspconfig_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/{folder_path}/tspconfig.yaml" + else: + # Get the most recent commit + latest_commit = commits[0].sha + logger.info(f"Found latest commit for {service_name}: {latest_commit}") + + # Construct the URL to the tspconfig.yaml file + tspconfig_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/{latest_commit}/{folder_path}/tspconfig.yaml" + + logger.info(f"tspconfig URL: {tspconfig_url}") + return tspconfig_url + + except Exception as e: + logger.error(f"Error getting latest commit: {str(e)}") + # Fallback to main branch if there's an error + fallback_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/{folder_path}/tspconfig.yaml" + logger.info(f"Falling back to URL: {fallback_url}") + return fallback_url + # Helper function to run CLI commands def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optional[str] = None) -> Dict[str, Any]: @@ -37,11 +88,12 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Run the command and capture the output result = subprocess.run( cli_args, - check=True, + # check=True, capture_output=True, text=True, - cwd=root_dir, + # cwd=root_dir, ) + logger.info(f"Command output: {result.stdout}") return { "success": True, @@ -50,6 +102,7 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio "code": result.returncode } except subprocess.CalledProcessError as e: + logger.error(f"Command failed with error: {str(e)}") return { "success": False, "stdout": e.stdout, @@ -60,183 +113,186 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Register tools for each TypeSpec client generator CLI command @mcp.tool("init") -def init_tool(tspconfig_url: Optional[str] = None, root_dir: Optional[str] = None) -> Dict[str, Any]: - """Initialize a client library directory using a tspconfig.yaml file. +def init_tool(package_name: str, root_dir: Optional[str] = None) -> Dict[str, Any]: + """Initialize a typespec client library directory given the package name. Args: - tspconfig_url: The URL of the tspconfig.yaml file. - root_dir: The root directory where the client library will be generated. # TODO: Unsure if this is needed + package_name: The name of the package to initialize. + root_dir: The root directory where the client library will be generated. Returns: A dictionary containing the result of the command. """ + # Get the URL to the tspconfig.yaml file + tspconfig_url = get_latest_commit(package_name) + + # Prepare arguments for the CLI command args = {} - if tspconfig_url: - args["tsp-config"] = tspconfig_url + args["tsp-config"] = "--tsp-config " + tspconfig_url return run_typespec_cli_command("init", args, root_dir=root_dir) -@mcp.tool("update") -def update_tool(path: Optional[str] = None) -> Dict[str, Any]: - """Look for a tsp-location.yaml file to sync a TypeSpec project and generate a client library. +# @mcp.tool("update") +# def update_tool(path: Optional[str] = None) -> Dict[str, Any]: +# """Look for a tsp-location.yaml file to sync a TypeSpec project and generate a client library. - Args: - path: Path to a tsp-location.yaml file or a directory containing one. +# Args: +# path: Path to a tsp-location.yaml file or a directory containing one. - Returns: - A dictionary containing the result of the command. - """ - args = {} - if path: - args["_"] = [path] +# Returns: +# A dictionary containing the result of the command. +# """ +# args = {} +# if path: +# args["_"] = [path] - return run_typespec_cli_command("update", args) +# return run_typespec_cli_command("update", args) -@mcp.tool("sync") -def sync_tool(repo_url: str, commit: Optional[str] = None, tag: Optional[str] = None, - branch: Optional[str] = None, path: Optional[str] = None) -> Dict[str, Any]: - """Sync a TypeSpec project with parameters specified in tsp-location.yaml. - - Args: - repo_url: URL of the repository containing the TypeSpec project. - commit: Commit hash to sync to. - tag: Tag to sync to. - branch: Branch to sync to. - path: Path within the repository to the TypeSpec project. - - Returns: - A dictionary containing the result of the command. - """ - args = { - "_": [repo_url] - } - - if commit: - args["commit"] = commit - if tag: - args["tag"] = tag - if branch: - args["branch"] = branch - if path: - args["path"] = path - - return run_typespec_cli_command("sync", args) +# @mcp.tool("sync") +# def sync_tool(repo_url: str, commit: Optional[str] = None, tag: Optional[str] = None, +# branch: Optional[str] = None, path: Optional[str] = None) -> Dict[str, Any]: +# """Sync a TypeSpec project with parameters specified in tsp-location.yaml. + +# Args: +# repo_url: URL of the repository containing the TypeSpec project. +# commit: Commit hash to sync to. +# tag: Tag to sync to. +# branch: Branch to sync to. +# path: Path within the repository to the TypeSpec project. + +# Returns: +# A dictionary containing the result of the command. +# """ +# args = { +# "_": [repo_url] +# } + +# if commit: +# args["commit"] = commit +# if tag: +# args["tag"] = tag +# if branch: +# args["branch"] = branch +# if path: +# args["path"] = path + +# return run_typespec_cli_command("sync", args) -@mcp.tool("generate") -def generate_tool(project_directory: str, output_directory: Optional[str] = None, - language: Optional[str] = None, tsp_emit: Optional[str] = None, - service_dir: Optional[str] = None) -> Dict[str, Any]: - """Generate a client library from a TypeSpec project. - - Args: - project_directory: The directory containing the TypeSpec project. - output_directory: The directory where the client library will be generated. - language: The language to generate client code for. - tsp_emit: The emitter to use for generating the client library. - service_dir: The directory containing the service definition. - - Returns: - A dictionary containing the result of the command. - """ - args = { - "_": [project_directory] - } - - if output_directory: - args["output-dir"] = output_directory - if language: - args["language"] = language - if tsp_emit: - args["tsp-emit"] = tsp_emit - if service_dir: - args["service-dir"] = service_dir - - return run_typespec_cli_command("generate", args) +# @mcp.tool("generate") +# def generate_tool(project_directory: str, output_directory: Optional[str] = None, +# language: Optional[str] = None, tsp_emit: Optional[str] = None, +# service_dir: Optional[str] = None) -> Dict[str, Any]: +# """Generate a client library from a TypeSpec project. + +# Args: +# project_directory: The directory containing the TypeSpec project. +# output_directory: The directory where the client library will be generated. +# language: The language to generate client code for. +# tsp_emit: The emitter to use for generating the client library. +# service_dir: The directory containing the service definition. + +# Returns: +# A dictionary containing the result of the command. +# """ +# args = { +# "_": [project_directory] +# } + +# if output_directory: +# args["output-dir"] = output_directory +# if language: +# args["language"] = language +# if tsp_emit: +# args["tsp-emit"] = tsp_emit +# if service_dir: +# args["service-dir"] = service_dir + +# return run_typespec_cli_command("generate", args) -@mcp.tool("convert") -def convert_tool(swagger_path: str, output_directory: Optional[str] = None, - title: Optional[str] = None, version: Optional[str] = None) -> Dict[str, Any]: - """Convert an existing swagger specification to a TypeSpec project. - - Args: - swagger_path: Path to the Swagger specification file. - output_directory: The directory where the TypeSpec project will be generated. - title: The title for the generated TypeSpec project. - version: The version for the generated TypeSpec project. - - Returns: - A dictionary containing the result of the command. - """ - args = { - "_": [swagger_path] - } - - if output_directory: - args["output-dir"] = output_directory - if title: - args["title"] = title - if version: - args["version"] = version - - return run_typespec_cli_command("convert", args) +# @mcp.tool("convert") +# def convert_tool(swagger_path: str, output_directory: Optional[str] = None, +# title: Optional[str] = None, version: Optional[str] = None) -> Dict[str, Any]: +# """Convert an existing swagger specification to a TypeSpec project. + +# Args: +# swagger_path: Path to the Swagger specification file. +# output_directory: The directory where the TypeSpec project will be generated. +# title: The title for the generated TypeSpec project. +# version: The version for the generated TypeSpec project. + +# Returns: +# A dictionary containing the result of the command. +# """ +# args = { +# "_": [swagger_path] +# } + +# if output_directory: +# args["output-dir"] = output_directory +# if title: +# args["title"] = title +# if version: +# args["version"] = version + +# return run_typespec_cli_command("convert", args) -@mcp.tool("compare") -def compare_tool(old_swagger: str, new_swagger: str, output_file: Optional[str] = None) -> Dict[str, Any]: - """Compare two Swagger definitions to identify relevant differences. - - Args: - old_swagger: Path to the old Swagger specification file. - new_swagger: Path to the new Swagger specification file. - output_file: Path to the output file where the comparison results will be written. - - Returns: - A dictionary containing the result of the command. - """ - args = { - "_": [old_swagger, new_swagger] - } - - if output_file: - args["output-file"] = output_file - - return run_typespec_cli_command("compare", args) +# @mcp.tool("compare") +# def compare_tool(old_swagger: str, new_swagger: str, output_file: Optional[str] = None) -> Dict[str, Any]: +# """Compare two Swagger definitions to identify relevant differences. + +# Args: +# old_swagger: Path to the old Swagger specification file. +# new_swagger: Path to the new Swagger specification file. +# output_file: Path to the output file where the comparison results will be written. + +# Returns: +# A dictionary containing the result of the command. +# """ +# args = { +# "_": [old_swagger, new_swagger] +# } + +# if output_file: +# args["output-file"] = output_file + +# return run_typespec_cli_command("compare", args) -@mcp.tool("sort_swagger") -def sort_swagger_tool(swagger_path: str, output_file: Optional[str] = None) -> Dict[str, Any]: - """Sort an existing swagger specification to match TypeSpec generated swagger order. +# @mcp.tool("sort_swagger") +# def sort_swagger_tool(swagger_path: str, output_file: Optional[str] = None) -> Dict[str, Any]: +# """Sort an existing swagger specification to match TypeSpec generated swagger order. - Args: - swagger_path: Path to the Swagger specification file. - output_file: Path to the output file where the sorted Swagger will be written. +# Args: +# swagger_path: Path to the Swagger specification file. +# output_file: Path to the output file where the sorted Swagger will be written. - Returns: - A dictionary containing the result of the command. - """ - args = { - "_": [swagger_path] - } +# Returns: +# A dictionary containing the result of the command. +# """ +# args = { +# "_": [swagger_path] +# } - if output_file: - args["output-file"] = output_file +# if output_file: +# args["output-file"] = output_file - return run_typespec_cli_command("sort-swagger", args) +# return run_typespec_cli_command("sort-swagger", args) -@mcp.tool("generate_config_files") -def generate_config_files_tool(output_directory: Optional[str] = None) -> Dict[str, Any]: - """Generate default configuration files used by tsp-client. +# @mcp.tool("generate_config_files") +# def generate_config_files_tool(output_directory: Optional[str] = None) -> Dict[str, Any]: +# """Generate default configuration files used by tsp-client. - Args: - output_directory: The directory where the configuration files will be generated. +# Args: +# output_directory: The directory where the configuration files will be generated. - Returns: - A dictionary containing the result of the command. - """ - args = {} +# Returns: +# A dictionary containing the result of the command. +# """ +# args = {} - if output_directory: - args["output-dir"] = output_directory +# if output_directory: +# args["output-dir"] = output_directory - return run_typespec_cli_command("generate-config-files", args) +# return run_typespec_cli_command("generate-config-files", args) # Run the MCP server if __name__ == "__main__": diff --git a/tools/mcp/typespec-story/pyproject.toml b/tools/mcp/typespec-story/pyproject.toml index eb52765a7adf..d57966ad5d96 100644 --- a/tools/mcp/typespec-story/pyproject.toml +++ b/tools/mcp/typespec-story/pyproject.toml @@ -2,7 +2,7 @@ name = "typespec-story-mcp" version = "0.1.0" description = "MCP server for TypeSpec client generator CLI" -requires-python = ">=3.12" +requires-python = ">=3.9" license = "MIT" readme = "README.md" authors = [ @@ -10,6 +10,7 @@ authors = [ ] dependencies = [ "mcp[cli]>=1.6.0", + "pygithub>=2.6.1", ] From 3aaf69c6d2f673de7c08cd9906fcde46d0b6e858 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 08:33:59 -0700 Subject: [PATCH 2/7] naming --- .vscode/mcp.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 1fbe01e66f20..9dd3c3fa81ad 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -4,7 +4,7 @@ "command": "uv", "args": [ "--directory", - "${workspaceFolder}/tools/mcp/typespec-story/", + "${workspaceFolder}/tools/mcp/typespec/", "run", "main.py" ] From 9ae27e758988ce9ed7d03de61250410d20de66d8 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 09:45:38 -0700 Subject: [PATCH 3/7] add ghtoken --- .vscode/mcp.json | 15 +++++++++++++-- tools/mcp/typespec-story/main.py | 7 +++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 9dd3c3fa81ad..b8c31695a45e 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,13 +1,24 @@ { + "inputs": [ + { + "type": "promptString", + "id": "gh_token", + "description": "GitHub Token", + "password": true + } + ], "servers": { "typespec-python": { "command": "uv", "args": [ "--directory", - "${workspaceFolder}/tools/mcp/typespec/", + "${workspaceFolder}/tools/mcp/typespec-story/", "run", "main.py" - ] + ], + "env": { + "GH_TOKEN": "${input:gh_token}" + }, }, "azure-sdk-validation": { "command": "uv", diff --git a/tools/mcp/typespec-story/main.py b/tools/mcp/typespec-story/main.py index 30a37195d4ac..c4fa21f3631e 100644 --- a/tools/mcp/typespec-story/main.py +++ b/tools/mcp/typespec-story/main.py @@ -3,7 +3,8 @@ from typing import Dict, Optional, Any import logging import sys -from github import Github +import os +from github import Github, Auth logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -34,7 +35,9 @@ def get_latest_commit(package_name: str) -> str: logger.info(f"Looking for commits in folder: {folder_path}") try: - g = Github() + auth = Auth.Token(os.getenv("GH_TOKEN")) + g = Github(auth=auth) + logger.info(f"Authenticated to GitHub with token: {g}") repo = g.get_repo("Azure/azure-rest-api-specs") # Get commits that affect the specific folder From 3ff618a07109d9313b107d1d73a650c16a6ab9c7 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 09:46:22 -0700 Subject: [PATCH 4/7] only 3.10 and above --- tools/mcp/typespec-story/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/mcp/typespec-story/pyproject.toml b/tools/mcp/typespec-story/pyproject.toml index d57966ad5d96..fb6317153951 100644 --- a/tools/mcp/typespec-story/pyproject.toml +++ b/tools/mcp/typespec-story/pyproject.toml @@ -2,7 +2,7 @@ name = "typespec-story-mcp" version = "0.1.0" description = "MCP server for TypeSpec client generator CLI" -requires-python = ">=3.9" +requires-python = ">=3.10" license = "MIT" readme = "README.md" authors = [ From a6958898597b0136f0afd1b51ea4fa814affd9d4 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 11:00:06 -0700 Subject: [PATCH 5/7] trying this --- tools/mcp/typespec-story/main.py | 106 ++++++++++++++++--------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/tools/mcp/typespec-story/main.py b/tools/mcp/typespec-story/main.py index c4fa21f3631e..ac8c04706a0f 100644 --- a/tools/mcp/typespec-story/main.py +++ b/tools/mcp/typespec-story/main.py @@ -1,3 +1,4 @@ +import re from mcp.server.fastmcp import FastMCP import subprocess from typing import Dict, Optional, Any @@ -14,7 +15,7 @@ # Initialize FastMCP server mcp = FastMCP("typespec") -def get_latest_commit(package_name: str) -> str: +def get_latest_commit(tspurl: str) -> str: """Get the URL to the tspconfig.yaml file for a given package name from the GitHub repository. Args: @@ -23,48 +24,57 @@ def get_latest_commit(package_name: str) -> str: Returns: The URL to the tspconfig.yaml file for the package in the Azure/azure-rest-api-specs repository. """ - # Extract the service name from the package name - # For example, extract "eventgrid" from "azure-eventgrid" - if package_name.startswith("azure-"): - service_name = package_name[len("azure-"):] - else: - service_name = package_name - - # Define the folder path in the repo - folder_path = f"specification/{service_name}" - logger.info(f"Looking for commits in folder: {folder_path}") - + # Extract the service name, repo, commit, and tspconfig path from the URL + # Example URL: https://github.com/Azure/azure-rest-api-specs/blob/main/specification/eventgrid/Azure.Messaging.EventGrid/tspconfig.yaml try: + # Use regex to validate and extract components from the URL + configUrl = tspurl + res = re.match( + r"^https://(?Pgithub|raw.githubusercontent).com/(?P[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?P[0-9a-f]{40})/(?P.*)/tspconfig.yaml$", + configUrl + ) + + if res: + groups = res.groupdict() + latest_commit = groups["commit"] + folder_path = groups["path"] + # Extract service name from path (usually the second component) + path_parts = folder_path.split("/") + service_name = path_parts[1] if len(path_parts) > 1 else "unknown" + logger.info(f"Using explicit commit from URL: {latest_commit}") + return latest_commit + + # Parse the URL to extract the path within the repository + repo_parts = tspurl.split("/") + repo_name = f"{repo_parts[3]}/{repo_parts[4]}" + + parts = tspurl.split("azure-rest-api-specs/blob/")[1].split("/") + parts.pop(0) # Remove the branch name (e.g., 'main') + + # Join all parts until the last directory (containing tspconfig.yaml) + folder_path = "/".join(parts[:-1]) + logger.info(f"Extracted folder path from URL: {folder_path}") + + # Extract service name for logging + service_name = parts[1] if len(parts) > 1 else "unknown" + + # try: auth = Auth.Token(os.getenv("GH_TOKEN")) g = Github(auth=auth) logger.info(f"Authenticated to GitHub with token: {g}") - repo = g.get_repo("Azure/azure-rest-api-specs") - + repo = g.get_repo(repo_name) + # Get commits that affect the specific folder commits = repo.get_commits(path=folder_path) - - if commits.totalCount == 0: - logger.warning(f"No commits found for path: {folder_path}") - # Fall back to main branch - tspconfig_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/{folder_path}/tspconfig.yaml" - else: - # Get the most recent commit - latest_commit = commits[0].sha - logger.info(f"Found latest commit for {service_name}: {latest_commit}") - - # Construct the URL to the tspconfig.yaml file - tspconfig_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/{latest_commit}/{folder_path}/tspconfig.yaml" - - logger.info(f"tspconfig URL: {tspconfig_url}") - return tspconfig_url - - except Exception as e: - logger.error(f"Error getting latest commit: {str(e)}") - # Fallback to main branch if there's an error - fallback_url = f"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/{folder_path}/tspconfig.yaml" - logger.info(f"Falling back to URL: {fallback_url}") - return fallback_url + latest_commit = commits[0].sha + logger.info(f"Found latest commit for {service_name}: {latest_commit}") + return latest_commit + + except Exception as e: + logger.error(f"An error occurred: {str(e)}") + raise + # Helper function to run CLI commands def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optional[str] = None) -> Dict[str, Any]: @@ -73,17 +83,8 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Convert args dict to CLI arguments for key, value in args.items(): - if key == "_": - # Handle positional arguments - if isinstance(value, list): - cli_args.extend(value) - else: - cli_args.append(value) - elif value is True: - cli_args.append(f"--{key}") - elif value is not False and value is not None: - cli_args.append(f"--{key}") - cli_args.append(str(value)) + cli_args.append(f"--{key}") + cli_args.append(str(value)) logger.info(f"Running command: {' '.join(cli_args)}") @@ -116,22 +117,23 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Register tools for each TypeSpec client generator CLI command @mcp.tool("init") -def init_tool(package_name: str, root_dir: Optional[str] = None) -> Dict[str, Any]: - """Initialize a typespec client library directory given the package name. +def init_tool(tsp_config_url: str, root_dir: Optional[str] = None) -> Dict[str, Any]: + """Initialize a typespec client library directory given the package url. Args: - package_name: The name of the package to initialize. + tsp_config_url: The URL to the tspconfig.yaml file for the package. root_dir: The root directory where the client library will be generated. Returns: A dictionary containing the result of the command. """ # Get the URL to the tspconfig.yaml file - tspconfig_url = get_latest_commit(package_name) + commit_id = get_latest_commit(tsp_config_url) # Prepare arguments for the CLI command args = {} - args["tsp-config"] = "--tsp-config " + tspconfig_url + args["tsp-config"] = tsp_config_url + args["commit"] = commit_id return run_typespec_cli_command("init", args, root_dir=root_dir) From bf6a61a5beacc377da30b657037f265c7889f4c8 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 11:42:58 -0700 Subject: [PATCH 6/7] url --- tools/mcp/typespec-story/main.py | 216 ++++--------------------------- 1 file changed, 25 insertions(+), 191 deletions(-) diff --git a/tools/mcp/typespec-story/main.py b/tools/mcp/typespec-story/main.py index ac8c04706a0f..87880fb6e2cd 100644 --- a/tools/mcp/typespec-story/main.py +++ b/tools/mcp/typespec-story/main.py @@ -1,7 +1,7 @@ import re from mcp.server.fastmcp import FastMCP import subprocess -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, Match, Union import logging import sys import os @@ -15,35 +15,30 @@ # Initialize FastMCP server mcp = FastMCP("typespec") -def get_latest_commit(tspurl: str) -> str: - """Get the URL to the tspconfig.yaml file for a given package name from the GitHub repository. +def get_latest_commit(tspurl: str) -> Union[Match[str], None]: + """Get the latest commit hash for a given TypeSpec config URL. Args: - package_name: The name of the package to get the latest commit for, e.g. "azure-eventgrid". + tspurl: The URL to the tspconfig.yaml file. Returns: - The URL to the tspconfig.yaml file for the package in the Azure/azure-rest-api-specs repository. + The latest commit hash for the specified TypeSpec configuration. """ # Extract the service name, repo, commit, and tspconfig path from the URL - # Example URL: https://github.com/Azure/azure-rest-api-specs/blob/main/specification/eventgrid/Azure.Messaging.EventGrid/tspconfig.yaml try: # Use regex to validate and extract components from the URL configUrl = tspurl res = re.match( - r"^https://(?Pgithub|raw.githubusercontent).com/(?P[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?P[0-9a-f]{40})/(?P.*)/tspconfig.yaml$", + r"^https://(?Pgithub|raw.githubusercontent).com/(?P[^/]*/azure-rest-api-specs(-pr)?)/(tree/|blob/)?(?P[0-9a-f]{40}|[^/]+)/(?P.*)/tspconfig.yaml$", configUrl ) + commit = None if res: groups = res.groupdict() - latest_commit = groups["commit"] - folder_path = groups["path"] - # Extract service name from path (usually the second component) - path_parts = folder_path.split("/") - service_name = path_parts[1] if len(path_parts) > 1 else "unknown" - logger.info(f"Using explicit commit from URL: {latest_commit}") - return latest_commit - + commit = groups["commit"] + logger.info(f"Extracted commit: {commit}") + # Parse the URL to extract the path within the repository repo_parts = tspurl.split("/") repo_name = f"{repo_parts[3]}/{repo_parts[4]}" @@ -52,24 +47,26 @@ def get_latest_commit(tspurl: str) -> str: parts.pop(0) # Remove the branch name (e.g., 'main') # Join all parts until the last directory (containing tspconfig.yaml) - folder_path = "/".join(parts[:-1]) + folder_path = "/".join(parts) logger.info(f"Extracted folder path from URL: {folder_path}") - - # Extract service name for logging - service_name = parts[1] if len(parts) > 1 else "unknown" - # try: auth = Auth.Token(os.getenv("GH_TOKEN")) g = Github(auth=auth) logger.info(f"Authenticated to GitHub with token: {g}") repo = g.get_repo(repo_name) # Get commits that affect the specific folder - commits = repo.get_commits(path=folder_path) + # TODO: if commit is a branch name + if not commit or commit == "main": + commits = repo.get_commits(path=folder_path) + + if not commits: + raise ValueError(f"No commits found for path: {folder_path}") - latest_commit = commits[0].sha - logger.info(f"Found latest commit for {service_name}: {latest_commit}") - return latest_commit + latest_commit = commits[0].sha + logger.info(f"Found latest commit for {latest_commit}") + return f"https://{groups['urlRoot']}.com/{groups['repo']}/blob/{latest_commit}/{groups['path']}/tspconfig.yaml" + return f"https://{groups['urlRoot']}.com/{groups['repo']}/blob/{commit}/{groups['path']}/tspconfig.yaml" except Exception as e: logger.error(f"An error occurred: {str(e)}") @@ -118,187 +115,24 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Register tools for each TypeSpec client generator CLI command @mcp.tool("init") def init_tool(tsp_config_url: str, root_dir: Optional[str] = None) -> Dict[str, Any]: - """Initialize a typespec client library directory given the package url. + """Initialize a typespec client library directory given the url. Args: - tsp_config_url: The URL to the tspconfig.yaml file for the package. + tsp_config_url: The URL to the tspconfig.yaml file. root_dir: The root directory where the client library will be generated. Returns: A dictionary containing the result of the command. """ # Get the URL to the tspconfig.yaml file - commit_id = get_latest_commit(tsp_config_url) + updated_url = get_latest_commit(tsp_config_url) # Prepare arguments for the CLI command args = {} - args["tsp-config"] = tsp_config_url - args["commit"] = commit_id + args["tsp-config"] = updated_url return run_typespec_cli_command("init", args, root_dir=root_dir) -# @mcp.tool("update") -# def update_tool(path: Optional[str] = None) -> Dict[str, Any]: -# """Look for a tsp-location.yaml file to sync a TypeSpec project and generate a client library. - -# Args: -# path: Path to a tsp-location.yaml file or a directory containing one. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = {} -# if path: -# args["_"] = [path] - -# return run_typespec_cli_command("update", args) - -# @mcp.tool("sync") -# def sync_tool(repo_url: str, commit: Optional[str] = None, tag: Optional[str] = None, -# branch: Optional[str] = None, path: Optional[str] = None) -> Dict[str, Any]: -# """Sync a TypeSpec project with parameters specified in tsp-location.yaml. - -# Args: -# repo_url: URL of the repository containing the TypeSpec project. -# commit: Commit hash to sync to. -# tag: Tag to sync to. -# branch: Branch to sync to. -# path: Path within the repository to the TypeSpec project. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = { -# "_": [repo_url] -# } - -# if commit: -# args["commit"] = commit -# if tag: -# args["tag"] = tag -# if branch: -# args["branch"] = branch -# if path: -# args["path"] = path - -# return run_typespec_cli_command("sync", args) - -# @mcp.tool("generate") -# def generate_tool(project_directory: str, output_directory: Optional[str] = None, -# language: Optional[str] = None, tsp_emit: Optional[str] = None, -# service_dir: Optional[str] = None) -> Dict[str, Any]: -# """Generate a client library from a TypeSpec project. - -# Args: -# project_directory: The directory containing the TypeSpec project. -# output_directory: The directory where the client library will be generated. -# language: The language to generate client code for. -# tsp_emit: The emitter to use for generating the client library. -# service_dir: The directory containing the service definition. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = { -# "_": [project_directory] -# } - -# if output_directory: -# args["output-dir"] = output_directory -# if language: -# args["language"] = language -# if tsp_emit: -# args["tsp-emit"] = tsp_emit -# if service_dir: -# args["service-dir"] = service_dir - -# return run_typespec_cli_command("generate", args) - -# @mcp.tool("convert") -# def convert_tool(swagger_path: str, output_directory: Optional[str] = None, -# title: Optional[str] = None, version: Optional[str] = None) -> Dict[str, Any]: -# """Convert an existing swagger specification to a TypeSpec project. - -# Args: -# swagger_path: Path to the Swagger specification file. -# output_directory: The directory where the TypeSpec project will be generated. -# title: The title for the generated TypeSpec project. -# version: The version for the generated TypeSpec project. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = { -# "_": [swagger_path] -# } - -# if output_directory: -# args["output-dir"] = output_directory -# if title: -# args["title"] = title -# if version: -# args["version"] = version - -# return run_typespec_cli_command("convert", args) - -# @mcp.tool("compare") -# def compare_tool(old_swagger: str, new_swagger: str, output_file: Optional[str] = None) -> Dict[str, Any]: -# """Compare two Swagger definitions to identify relevant differences. - -# Args: -# old_swagger: Path to the old Swagger specification file. -# new_swagger: Path to the new Swagger specification file. -# output_file: Path to the output file where the comparison results will be written. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = { -# "_": [old_swagger, new_swagger] -# } - -# if output_file: -# args["output-file"] = output_file - -# return run_typespec_cli_command("compare", args) - -# @mcp.tool("sort_swagger") -# def sort_swagger_tool(swagger_path: str, output_file: Optional[str] = None) -> Dict[str, Any]: -# """Sort an existing swagger specification to match TypeSpec generated swagger order. - -# Args: -# swagger_path: Path to the Swagger specification file. -# output_file: Path to the output file where the sorted Swagger will be written. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = { -# "_": [swagger_path] -# } - -# if output_file: -# args["output-file"] = output_file - -# return run_typespec_cli_command("sort-swagger", args) - -# @mcp.tool("generate_config_files") -# def generate_config_files_tool(output_directory: Optional[str] = None) -> Dict[str, Any]: -# """Generate default configuration files used by tsp-client. - -# Args: -# output_directory: The directory where the configuration files will be generated. - -# Returns: -# A dictionary containing the result of the command. -# """ -# args = {} - -# if output_directory: -# args["output-dir"] = output_directory - -# return run_typespec_cli_command("generate-config-files", args) - # Run the MCP server if __name__ == "__main__": # Initialize and run the server From ba757b264e735bb4af8e3d9ad5921a9136fb2469 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Wed, 30 Apr 2025 13:50:00 -0700 Subject: [PATCH 7/7] fixed --- tools/mcp/typespec-story/main.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tools/mcp/typespec-story/main.py b/tools/mcp/typespec-story/main.py index 87880fb6e2cd..d96d89edded7 100644 --- a/tools/mcp/typespec-story/main.py +++ b/tools/mcp/typespec-story/main.py @@ -65,8 +65,8 @@ def get_latest_commit(tspurl: str) -> Union[Match[str], None]: latest_commit = commits[0].sha logger.info(f"Found latest commit for {latest_commit}") - return f"https://{groups['urlRoot']}.com/{groups['repo']}/blob/{latest_commit}/{groups['path']}/tspconfig.yaml" - return f"https://{groups['urlRoot']}.com/{groups['repo']}/blob/{commit}/{groups['path']}/tspconfig.yaml" + return f"https://raw.githubusercontent.com/{groups['repo']}/{latest_commit}/{groups['path']}/tspconfig.yaml" + return f"https://raw.githubusercontent.com/{groups['repo']}/{commit}/{groups['path']}/tspconfig.yaml" except Exception as e: logger.error(f"An error occurred: {str(e)}") @@ -89,10 +89,9 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio # Run the command and capture the output result = subprocess.run( cli_args, - # check=True, + check=True, capture_output=True, text=True, - # cwd=root_dir, ) logger.info(f"Command output: {result.stdout}") @@ -102,15 +101,10 @@ def run_typespec_cli_command(command: str, args: Dict[str, Any], root_dir: Optio "stderr": result.stderr, "code": result.returncode } - except subprocess.CalledProcessError as e: + except Exception as e: logger.error(f"Command failed with error: {str(e)}") - return { - "success": False, - "stdout": e.stdout, - "stderr": e.stderr, - "code": e.returncode, - "error": str(e) - } + raise + # Register tools for each TypeSpec client generator CLI command @mcp.tool("init")