Skip to content
46 changes: 46 additions & 0 deletions tools/Mcp/src/services/toolsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import path from 'path';
import { get, RequestOptions } from 'http';
import { toolParameterSchema } from '../types.js';
import { CodegenServer } from '../CodegenServer.js';
import {
listSpecModules,
listProvidersForService,
listApiVersions,
resolveAutorestInputs
} from './utils.js';

export class ToolsService {
private static _instance: ToolsService;
Expand Down Expand Up @@ -42,6 +48,18 @@ export class ToolsService {
case "createTestsFromSpecs":
func = this.createTestsFromSpecs<Args>;
break;
case "listSpecModules":
func = this.toolListSpecModules<Args>;
break;
case "listProvidersForService":
func = this.toolListProvidersForService<Args>;
break;
case "listApiVersions":
func = this.toolListApiVersions<Args>;
break;
case "resolveAutorestInputs":
func = this.toolResolveAutorestInputs<Args>;
break;
default:
throw new Error(`Tool ${name} not found`);
}
Expand Down Expand Up @@ -167,4 +185,32 @@ export class ToolsService {
const exampleSpecsPath = await utils.getExamplesFromSpecs(workingDirectory);
return [exampleSpecsPath, testPath];
}

toolListSpecModules = async <Args extends ZodRawShape>(_args: Args): Promise<string[]> => {
const modules = await listSpecModules();
return [JSON.stringify(modules)];
}

toolListProvidersForService = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
const service = z.string().parse(Object.values(args)[0]);
const providers = await listProvidersForService(service);
return [service, JSON.stringify(providers)];
}

toolListApiVersions = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
const service = z.string().parse(Object.values(args)[0]);
const provider = z.string().parse(Object.values(args)[1]);
const res = await listApiVersions(service, provider);
return [service, provider, JSON.stringify(res.stable), JSON.stringify(res.preview)];
}

toolResolveAutorestInputs = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
const service = z.string().parse(Object.values(args)[0]);
const provider = z.string().parse(Object.values(args)[1]);
const stability = z.enum(['stable','preview']).parse(Object.values(args)[2]);
const version = z.string().parse(Object.values(args)[3]);
const swaggerPath = Object.values(args)[4] ? z.string().parse(Object.values(args)[4]) : undefined;
const resolved = await resolveAutorestInputs({ service, provider, stability, version, swaggerPath });
return [resolved.serviceName, resolved.commitId, resolved.serviceSpecs, resolved.swaggerFileSpecs];
}
}
123 changes: 123 additions & 0 deletions tools/Mcp/src/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { yamlContent } from '../types.js';
import { execSync } from 'child_process';
import path from 'path';

const GITHUB_API_BASE = 'https://api.github.com';
const REST_API_SPECS_OWNER = 'Azure';
const REST_API_SPECS_REPO = 'azure-rest-api-specs';

const _pwshCD = (path: string): string => { return `pwsh -Command "$path = resolve-path ${path} | Set-Location"` }
const _autorestReset = "autorest --reset"
const _autorest = "autorest"
Expand Down Expand Up @@ -78,6 +82,125 @@ export async function getSwaggerContentFromUrl(swaggerUrl: string): Promise<any>
}
}

/**
* GitHub helper: get latest commit SHA for azure-rest-api-specs main branch
*/
export async function getSpecsHeadCommitSha(branch: string = 'main'): Promise<string> {
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/branches/${branch}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch branch '${branch}' info: ${res.status}`);
}
const data = await res.json();
return data?.commit?.sha as string;
}

/**
* List top-level service directories under specification/
*/
export async function listSpecModules(): Promise<string[]> {
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/specification`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to list specification directory: ${res.status}`);
}
const list = await res.json();
return (Array.isArray(list) ? list : [])
.filter((e: any) => e.type === 'dir')
.map((e: any) => e.name)
.sort((a: string, b: string) => a.localeCompare(b));
}

/**
* Given a service (spec folder), list provider namespaces under resource-manager.
*/
export async function listProvidersForService(service: string): Promise<string[]> {
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/specification/${service}/resource-manager`;
const res = await fetch(url);
if (!res.ok) {
// Sometimes service has alternate structure or doesn't exist
throw new Error(`Failed to list providers for service '${service}': ${res.status}`);
}
const list = await res.json();
return (Array.isArray(list) ? list : [])
.filter((e: any) => e.type === 'dir')
.map((e: any) => e.name)
.sort((a: string, b: string) => a.localeCompare(b));
}

/**
* For service + provider, list API version directories under stable/ and preview/.
* Returns map: { stable: string[], preview: string[] }
*/
export async function listApiVersions(service: string, provider: string): Promise<{ stable: string[]; preview: string[] }> {
const base = `specification/${service}/resource-manager/${provider}`;
const folders = ['stable', 'preview'] as const;
const result: { stable: string[]; preview: string[] } = { stable: [], preview: [] };
for (const f of folders) {
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/${base}/${f}`;
const res = await fetch(url);
if (!res.ok) {
// ignore missing
continue;
}
const list = await res.json();
const versions = (Array.isArray(list) ? list : [])
.filter((e: any) => e.type === 'dir')
.map((e: any) => e.name)
.sort((a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }));
result[f] = versions;
}
return result;
}

/**
* For a given service/provider/version, find likely swagger files (.json) under that version path.
* Returns array of repo-relative file paths (starting with specification/...).
*/
export async function listSwaggerFiles(service: string, provider: string, stability: 'stable'|'preview', version: string): Promise<string[]> {
const dir = `specification/${service}/resource-manager/${provider}/${stability}/${version}`;
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/${dir}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to list files for ${dir}: ${res.status}`);
}
const list = await res.json();
const files: any[] = Array.isArray(list) ? list : [];
// Find JSON files; prefer names ending with provider or service
const jsons = files.filter(f => f.type === 'file' && f.name.endsWith('.json'));
const preferred = jsons.filter(f => new RegExp(`${provider.split('.').pop()}|${service}`, 'i').test(f.name));
const ordered = (preferred.length ? preferred : jsons).map(f => f.path);
return ordered;
}

/**
* Resolve the four Autorest inputs given service, provider, and version path.
*/
export async function resolveAutorestInputs(params: {
service: string;
provider: string;
stability: 'stable'|'preview';
version: string;
swaggerPath?: string; // optional repo-relative path override
}): Promise<{ serviceName: string; commitId: string; serviceSpecs: string; swaggerFileSpecs: string }> {
const commitId = await getSpecsHeadCommitSha('main');
const serviceSpecs = `${params.service}/resource-manager`;
let swaggerFileSpecs = params.swaggerPath ?? '';
if (!swaggerFileSpecs) {
const candidates = await listSwaggerFiles(params.service, params.provider, params.stability, params.version);
if (candidates.length === 0) {
throw new Error(`No swagger files found for ${params.service}/${params.provider}/${params.stability}/${params.version}`);
}
swaggerFileSpecs = candidates[0];
}
return {
serviceName: params.provider.replace(/^Microsoft\./, ''),
commitId,
serviceSpecs,
swaggerFileSpecs
};
}

export async function findAllPolyMorphism(workingDirectory: string): Promise<Map<string, Set<string>>> {
const polymorphism = new Map<string, Set<string>>();
const moduleReadmePath = path.join(workingDirectory, "README.md");
Expand Down
40 changes: 24 additions & 16 deletions tools/Mcp/src/specs/prompts/partner-module-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,33 @@

# Instructions

## Stage 1: Capturing Placeholder Values
- Ask the user for the following placeholder values: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
- Examples:
- serviceName: HybridConnectivity
- commitId: <commit hash of the swagger>
- serviceSpecs: hybridconnectivity/resource-manager
- swaggerFileSpecs: hybridconnectivity/resource-manager/Microsoft.HybridConnectivity/stable/2024-12-01/hybridconnectivity.json
- Do not replace or modify this prompt file.
- Store the values for use in later steps like generating the README and executing Autorest.
- Once values are stored, mark Stage 1 as complete.
## Stage 1: Interactive spec selection and autorest resolution
- Ask the user for their desired **PowerShell module name** (e.g., "HybridConnectivity")
- Call the MCP tool "list-spec-modules" to fetch all available specification folders from azure-rest-api-specs/specification.
- From the full list, present 10 most relevant spec options to the user based on their PowerShell module name, or show a representative sample if no clear match.
- Ask the user to choose which specification they want to use from the presented options, or ask if they want to see more options.
- **Confirm the spec choice**: Once user selects a spec, ask them to confirm this is the correct specification for their needs (show the spec name clearly).
- Call the MCP tool "list-providers" with the chosen spec folder to retrieve available provider namespaces.
- Present the list of providers to the user:
- If multiple providers are returned, ask the user to pick one
- If only one provider exists, select it automatically but confirm with the user
- **Confirm the provider choice**: Ask the user to confirm this is the correct provider namespace.
- Call the MCP tool "list-api-versions" with the chosen spec folder and provider to get available versions, separated by Stable and Preview.
- Present the API version options to the user and ask them to choose:
1. **Stability**: stable or preview
2. **API version**: specific version from the available list
- **Confirm the API version choice**: Ask the user to confirm their stability and version selection.
- Call the MCP tool "resolve-autorest-inputs" with the chosen spec folder, provider, stability, and version to compute the 4 autorest inputs: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
- Store the resolved values for later steps (README generation and Autorest). Mark Stage 1 complete.

## Stage 2: Generating partner powershell module
- FOLLOW ALL THE STEPS. DO NOT SKIP ANY STEPS.
- Navigate to the `src` folder in the home "azure-powershell" directory.
- Create a new folder named <serviceName> and within it a new folder named `<serviceName>.Autorest`. (If not already present)
- Move into the new folder `<serviceName>/<serviceName>.Autorest`, using the command `cd <serviceName>/<serviceName>.Autorest`.
- Create a new folder named <PowerShell module name> and within it a new folder named `<PowerShell module name>.Autorest`. (If not already present)
- Move into the new folder `<PowerShell module name>/<PowerShell module name>.Autorest`, using the command `cd <PowerShell module name>/<PowerShell module name>.Autorest`.
- Create a new file `README.md`. (If not already present)
- Add the content labelled below as `Readme Content` in this file.
- Use the "generate-autorest" mcp tool to generate the <serviceName> module.
- Use the "generate-autorest" mcp tool to generate the <PowerShell module name> module.
- Stage 2 Complete.

## Stage 3: Updating Example Files
Expand Down Expand Up @@ -74,12 +82,12 @@ try-require:
- $(repo)/specification/<serviceSpecs>/readme.powershell.md

input-file:
- $(repo)/<specification/<swaggerFileSpecs>
- $(repo)/specification/<swaggerFileSpecs>

module-version: 0.1.0

title: <serviceName>
service-name: <serviceName>
title: <PowerShell module name>
service-name: <PowerShell module name>
subject-prefix: $(service-name)

directive:
Expand Down
20 changes: 20 additions & 0 deletions tools/Mcp/src/specs/responses.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@
"type": "tool",
"text": "Read examples from specs are under {0}. Implement empty test stubs under {1}. Test stubs are named as '.Test.ps1'. Define variables in function 'setupEnv' in 'utils.ps1' under {1}, and use these variables for test cases. Value of these variables are from {0}. Leave test cases as empty if you don't find any matches. You are expert in Azure-PowerShell and Autorest.PowerShell, You know how to map data from {0} to {1}. "
},
{
"name": "list-spec-modules",
"type": "tool",
"text": "Available modules under azure-rest-api-specs/specification: {0}"
},
{
"name": "list-providers",
"type": "tool",
"text": "Providers for service {0}: {1}"
},
{
"name": "list-api-versions",
"type": "tool",
"text": "API versions for {0}/{1} — Stable: {2} | Preview: {3}"
},
{
"name": "resolve-autorest-inputs",
"type": "tool",
"text": "Resolved inputs — serviceName: {0}, commitId: {1}, serviceSpecs: {2}, swaggerFileSpecs: {3}"
},
{
"name": "create-greeting",
"type": "prompt",
Expand Down
34 changes: 34 additions & 0 deletions tools/Mcp/src/specs/specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@
}
],
"callbackName": "createTestsFromSpecs"
},
{
"name": "list-spec-modules",
"description": "List all top-level modules (service folders) under azure-rest-api-specs/specification.",
"parameters": [],
"callbackName": "listSpecModules"
},
{
"name": "list-providers",
"description": "List provider namespaces for a given service under resource-manager.",
"parameters": [
{ "name": "service", "description": "Service folder name under specification (e.g., hybridconnectivity)", "type": "string" }
],
"callbackName": "listProvidersForService"
},
{
"name": "list-api-versions",
"description": "List available API versions for a given service and provider (stable/preview).",
"parameters": [
{ "name": "service", "description": "Service folder name under specification", "type": "string" },
{ "name": "provider", "description": "Provider namespace folder under the service (e.g., Microsoft.HybridConnectivity)", "type": "string" }
],
"callbackName": "listApiVersions"
},
{
"name": "resolve-autorest-inputs",
"description": "Resolve the four Autorest inputs (serviceName, commitId, serviceSpecs, swaggerFileSpecs) from service/provider/version.",
"parameters": [
{ "name": "service", "description": "Service folder name under specification", "type": "string" },
{ "name": "provider", "description": "Provider namespace under the service", "type": "string" },
{ "name": "stability", "description": "'stable' or 'preview'", "type": "string" },
{ "name": "version", "description": "API version (e.g., 2024-12-01)", "type": "string" }
],
"callbackName": "resolveAutorestInputs"
}
],
"prompts": [
Expand Down
38 changes: 23 additions & 15 deletions tools/Mcp/test/vscode/mcpprompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,33 @@

# Instructions

## Stage 1: Capturing Placeholder Values
- Ask the user for the following placeholder values: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
- Examples:
- serviceName: HybridConnectivity
- commitId: <commit hash of the swagger>
- serviceSpecs: hybridconnectivity/resource-manager
- swaggerFileSpecs: hybridconnectivity/resource-manager/Microsoft.HybridConnectivity/stable/2024-12-01/hybridconnectivity.json
- Do not replace or modify this prompt file.
- Store the values for use in later steps like generating the README and executing Autorest.
- Once values are stored, mark Stage 1 as complete.
## Stage 1: Interactive spec selection and autorest resolution
- Ask the user for their desired **PowerShell module name** (e.g., "HybridConnectivity")
- Call the MCP tool "list-spec-modules" to fetch all available specification folders from azure-rest-api-specs/specification.
- From the full list, present 10 most relevant spec options to the user based on their PowerShell module name, or show a representative sample if no clear match.
- Ask the user to choose which specification they want to use from the presented options, or ask if they want to see more options.
- **Confirm the spec choice**: Once user selects a spec, ask them to confirm this is the correct specification for their needs (show the spec name clearly).
- Call the MCP tool "list-providers" with the chosen spec folder to retrieve available provider namespaces.
- Present the list of providers to the user:
- If multiple providers are returned, ask the user to pick one
- If only one provider exists, select it automatically but confirm with the user
- **Confirm the provider choice**: Ask the user to confirm this is the correct provider namespace.
- Call the MCP tool "list-api-versions" with the chosen spec folder and provider to get available versions, separated by Stable and Preview.
- Present the API version options to the user and ask them to choose:
1. **Stability**: stable or preview
2. **API version**: specific version from the available list
- **Confirm the API version choice**: Ask the user to confirm their stability and version selection.
- Call the MCP tool "resolve-autorest-inputs" with the chosen spec folder, provider, stability, and version to compute the 4 autorest inputs: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
- Store the resolved values for later steps (README generation and Autorest). Mark Stage 1 complete.

## Stage 2: Generating partner powershell module
- FOLLOW ALL THE STEPS. DO NOT SKIP ANY STEPS.
- Navigate to the `src` folder in the home "azure-powershell" directory.
- Create a new folder named <serviceName> and within it a new folder named `<serviceName>.Autorest`. (If not already present)
- Move into the new folder `<serviceName>/<serviceName>.Autorest`, using the command `cd <serviceName>/<serviceName>.Autorest`.
- Create a new folder named <PowerShell module name> and within it a new folder named `<PowerShell module name>.Autorest`. (If not already present)
- Move into the new folder `<PowerShell module name>/<PowerShell module name>.Autorest`, using the command `cd <PowerShell module name>/<PowerShell module name>.Autorest`.
- Create a new file `README.md`. (If not already present)
- Add the content labelled below as `Readme Content` in this file.
- Use the "generate-autorest" mcp tool to generate the <serviceName> module.
- Use the "generate-autorest" mcp tool to generate the <PowerShell module name> module.
- Stage 2 Complete.

## Stage 3: Updating Example Files
Expand Down Expand Up @@ -78,8 +86,8 @@ input-file:

module-version: 0.1.0

title: <serviceName>
service-name: <serviceName>
title: <PowerShell module name>
service-name: <PowerShell module name>
subject-prefix: $(service-name)

directive:
Expand Down
Loading