Skip to content

Commit 06421e4

Browse files
committed
Fuzzy Search
1 parent deef756 commit 06421e4

File tree

5 files changed

+235
-12
lines changed

5 files changed

+235
-12
lines changed

tools/Mcp/src/services/toolsService.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import path from 'path';
55
import { get, RequestOptions } from 'http';
66
import { toolParameterSchema } from '../types.js';
77
import { CodegenServer } from '../CodegenServer.js';
8+
import {
9+
listSpecModules,
10+
listProvidersForService,
11+
listApiVersions,
12+
resolveAutorestInputs
13+
} from './utils.js';
814

915
export class ToolsService {
1016
private static _instance: ToolsService;
@@ -42,6 +48,18 @@ export class ToolsService {
4248
case "createTestsFromSpecs":
4349
func = this.createTestsFromSpecs<Args>;
4450
break;
51+
case "listSpecModules":
52+
func = this.toolListSpecModules<Args>;
53+
break;
54+
case "listProvidersForService":
55+
func = this.toolListProvidersForService<Args>;
56+
break;
57+
case "listApiVersions":
58+
func = this.toolListApiVersions<Args>;
59+
break;
60+
case "resolveAutorestInputs":
61+
func = this.toolResolveAutorestInputs<Args>;
62+
break;
4563
default:
4664
throw new Error(`Tool ${name} not found`);
4765
}
@@ -167,4 +185,32 @@ export class ToolsService {
167185
const exampleSpecsPath = await utils.getExamplesFromSpecs(workingDirectory);
168186
return [exampleSpecsPath, testPath];
169187
}
188+
189+
toolListSpecModules = async <Args extends ZodRawShape>(_args: Args): Promise<string[]> => {
190+
const modules = await listSpecModules();
191+
return [JSON.stringify(modules)];
192+
}
193+
194+
toolListProvidersForService = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
195+
const service = z.string().parse(Object.values(args)[0]);
196+
const providers = await listProvidersForService(service);
197+
return [service, JSON.stringify(providers)];
198+
}
199+
200+
toolListApiVersions = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
201+
const service = z.string().parse(Object.values(args)[0]);
202+
const provider = z.string().parse(Object.values(args)[1]);
203+
const res = await listApiVersions(service, provider);
204+
return [service, provider, JSON.stringify(res.stable), JSON.stringify(res.preview)];
205+
}
206+
207+
toolResolveAutorestInputs = async <Args extends ZodRawShape>(args: Args): Promise<string[]> => {
208+
const service = z.string().parse(Object.values(args)[0]);
209+
const provider = z.string().parse(Object.values(args)[1]);
210+
const stability = z.enum(['stable','preview']).parse(Object.values(args)[2]);
211+
const version = z.string().parse(Object.values(args)[3]);
212+
const swaggerPath = Object.values(args)[4] ? z.string().parse(Object.values(args)[4]) : undefined;
213+
const resolved = await resolveAutorestInputs({ service, provider, stability, version, swaggerPath });
214+
return [resolved.serviceName, resolved.commitId, resolved.serviceSpecs, resolved.swaggerFileSpecs];
215+
}
170216
}

tools/Mcp/src/services/utils.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { yamlContent } from '../types.js';
44
import { execSync } from 'child_process';
55
import path from 'path';
66

7+
const GITHUB_API_BASE = 'https://api.github.com';
8+
const REST_API_SPECS_OWNER = 'Azure';
9+
const REST_API_SPECS_REPO = 'azure-rest-api-specs';
10+
711
const _pwshCD = (path: string): string => { return `pwsh -Command "$path = resolve-path ${path} | Set-Location"` }
812
const _autorestReset = "autorest --reset"
913
const _autorest = "autorest"
@@ -78,6 +82,125 @@ export async function getSwaggerContentFromUrl(swaggerUrl: string): Promise<any>
7882
}
7983
}
8084

85+
/**
86+
* GitHub helper: get latest commit SHA for azure-rest-api-specs main branch
87+
*/
88+
export async function getSpecsHeadCommitSha(branch: string = 'main'): Promise<string> {
89+
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/branches/${branch}`;
90+
const res = await fetch(url);
91+
if (!res.ok) {
92+
throw new Error(`Failed to fetch branch '${branch}' info: ${res.status}`);
93+
}
94+
const data = await res.json();
95+
return data?.commit?.sha as string;
96+
}
97+
98+
/**
99+
* List top-level service directories under specification/
100+
*/
101+
export async function listSpecModules(): Promise<string[]> {
102+
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/specification`;
103+
const res = await fetch(url);
104+
if (!res.ok) {
105+
throw new Error(`Failed to list specification directory: ${res.status}`);
106+
}
107+
const list = await res.json();
108+
return (Array.isArray(list) ? list : [])
109+
.filter((e: any) => e.type === 'dir')
110+
.map((e: any) => e.name)
111+
.sort((a: string, b: string) => a.localeCompare(b));
112+
}
113+
114+
/**
115+
* Given a service (spec folder), list provider namespaces under resource-manager.
116+
*/
117+
export async function listProvidersForService(service: string): Promise<string[]> {
118+
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/specification/${service}/resource-manager`;
119+
const res = await fetch(url);
120+
if (!res.ok) {
121+
// Sometimes service has alternate structure or doesn't exist
122+
throw new Error(`Failed to list providers for service '${service}': ${res.status}`);
123+
}
124+
const list = await res.json();
125+
return (Array.isArray(list) ? list : [])
126+
.filter((e: any) => e.type === 'dir')
127+
.map((e: any) => e.name)
128+
.sort((a: string, b: string) => a.localeCompare(b));
129+
}
130+
131+
/**
132+
* For service + provider, list API version directories under stable/ and preview/.
133+
* Returns map: { stable: string[], preview: string[] }
134+
*/
135+
export async function listApiVersions(service: string, provider: string): Promise<{ stable: string[]; preview: string[] }> {
136+
const base = `specification/${service}/resource-manager/${provider}`;
137+
const folders = ['stable', 'preview'] as const;
138+
const result: { stable: string[]; preview: string[] } = { stable: [], preview: [] };
139+
for (const f of folders) {
140+
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/${base}/${f}`;
141+
const res = await fetch(url);
142+
if (!res.ok) {
143+
// ignore missing
144+
continue;
145+
}
146+
const list = await res.json();
147+
const versions = (Array.isArray(list) ? list : [])
148+
.filter((e: any) => e.type === 'dir')
149+
.map((e: any) => e.name)
150+
.sort((a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }));
151+
result[f] = versions;
152+
}
153+
return result;
154+
}
155+
156+
/**
157+
* For a given service/provider/version, find likely swagger files (.json) under that version path.
158+
* Returns array of repo-relative file paths (starting with specification/...).
159+
*/
160+
export async function listSwaggerFiles(service: string, provider: string, stability: 'stable'|'preview', version: string): Promise<string[]> {
161+
const dir = `specification/${service}/resource-manager/${provider}/${stability}/${version}`;
162+
const url = `${GITHUB_API_BASE}/repos/${REST_API_SPECS_OWNER}/${REST_API_SPECS_REPO}/contents/${dir}`;
163+
const res = await fetch(url);
164+
if (!res.ok) {
165+
throw new Error(`Failed to list files for ${dir}: ${res.status}`);
166+
}
167+
const list = await res.json();
168+
const files: any[] = Array.isArray(list) ? list : [];
169+
// Find JSON files; prefer names ending with provider or service
170+
const jsons = files.filter(f => f.type === 'file' && f.name.endsWith('.json'));
171+
const preferred = jsons.filter(f => new RegExp(`${provider.split('.').pop()}|${service}`, 'i').test(f.name));
172+
const ordered = (preferred.length ? preferred : jsons).map(f => f.path);
173+
return ordered;
174+
}
175+
176+
/**
177+
* Resolve the four Autorest inputs given service, provider, and version path.
178+
*/
179+
export async function resolveAutorestInputs(params: {
180+
service: string;
181+
provider: string;
182+
stability: 'stable'|'preview';
183+
version: string;
184+
swaggerPath?: string; // optional repo-relative path override
185+
}): Promise<{ serviceName: string; commitId: string; serviceSpecs: string; swaggerFileSpecs: string }> {
186+
const commitId = await getSpecsHeadCommitSha('main');
187+
const serviceSpecs = `${params.service}/resource-manager`;
188+
let swaggerFileSpecs = params.swaggerPath ?? '';
189+
if (!swaggerFileSpecs) {
190+
const candidates = await listSwaggerFiles(params.service, params.provider, params.stability, params.version);
191+
if (candidates.length === 0) {
192+
throw new Error(`No swagger files found for ${params.service}/${params.provider}/${params.stability}/${params.version}`);
193+
}
194+
swaggerFileSpecs = candidates[0];
195+
}
196+
return {
197+
serviceName: params.provider.replace(/^Microsoft\./, ''),
198+
commitId,
199+
serviceSpecs,
200+
swaggerFileSpecs
201+
};
202+
}
203+
81204
export async function findAllPolyMorphism(workingDirectory: string): Promise<Map<string, Set<string>>> {
82205
const polymorphism = new Map<string, Set<string>>();
83206
const moduleReadmePath = path.join(workingDirectory, "README.md");

tools/Mcp/src/specs/prompts/partner-module-workflow.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212

1313
# Instructions
1414

15-
## Stage 1: Capturing Placeholder Values
16-
- Ask the user for the following placeholder values: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
17-
- Examples:
18-
- serviceName: HybridConnectivity
19-
- commitId: <commit hash of the swagger>
20-
- serviceSpecs: hybridconnectivity/resource-manager
21-
- swaggerFileSpecs: hybridconnectivity/resource-manager/Microsoft.HybridConnectivity/stable/2024-12-01/hybridconnectivity.json
22-
- Do not replace or modify this prompt file.
23-
- Store the values for use in later steps like generating the README and executing Autorest.
24-
- Once values are stored, mark Stage 1 as complete.
15+
## Stage 1: Fuzzy selection and autorest inputs (reduced user input)
16+
- Ask the user for only the approximate Azure service/module name (e.g., "hybrid connectivity").
17+
- Call the MCP tool "list-spec-modules" to fetch all service folders from azure-rest-api-specs/specification.
18+
- Fuzzily match the user's input to the closest service name. Show top 3 matches and ask the user to confirm the service folder to use.
19+
- Call the MCP tool "list-providers" with the chosen service to retrieve provider namespaces. If multiple providers are returned, ask the user to pick one; if only one, select it automatically.
20+
- Ask the user what they want to call the PowerShell module title/service-name (e.g., HybridConnectivity). This is the display/module name, not the spec folder name.
21+
- Call the MCP tool "list-api-versions" with service and provider to get available versions, separated by Stable and Preview. Ask the user to choose stability (stable/preview) and a specific API version.
22+
- Call the MCP tool "resolve-autorest-inputs" with service, provider, stability, and version to compute the 4 inputs: serviceName, commitId, serviceSpecs, swaggerFileSpecs.
23+
- Store the resolved values for later steps (README generation and Autorest). Mark Stage 1 complete.
2524

2625
## Stage 2: Generating partner powershell module
2726
- FOLLOW ALL THE STEPS. DO NOT SKIP ANY STEPS.
@@ -74,7 +73,7 @@ try-require:
7473
- $(repo)/specification/<serviceSpecs>/readme.powershell.md
7574

7675
input-file:
77-
- $(repo)/<specification/<swaggerFileSpecs>
76+
- $(repo)/specification/<swaggerFileSpecs>
7877

7978
module-version: 0.1.0
8079

tools/Mcp/src/specs/responses.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@
2929
"type": "tool",
3030
"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}. "
3131
},
32+
{
33+
"name": "list-spec-modules",
34+
"type": "tool",
35+
"text": "Available modules under azure-rest-api-specs/specification: {0}"
36+
},
37+
{
38+
"name": "list-providers",
39+
"type": "tool",
40+
"text": "Providers for service {0}: {1}"
41+
},
42+
{
43+
"name": "list-api-versions",
44+
"type": "tool",
45+
"text": "API versions for {0}/{1} — Stable: {2} | Preview: {3}"
46+
},
47+
{
48+
"name": "resolve-autorest-inputs",
49+
"type": "tool",
50+
"text": "Resolved inputs — serviceName: {0}, commitId: {1}, serviceSpecs: {2}, swaggerFileSpecs: {3}"
51+
},
3252
{
3353
"name": "create-greeting",
3454
"type": "prompt",
@@ -37,6 +57,6 @@
3757
{
3858
"name": "partner-module-workflow",
3959
"type": "prompt",
40-
"text": "@file:prompts/partner-module-workflow.md"
60+
"text": "@file:prompts/partner-module-workflow.md"
4161
}
4262
]

tools/Mcp/src/specs/specs.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@
7272
],
7373
"callbackName": "createTestsFromSpecs"
7474
}
75+
,
76+
{
77+
"name": "list-spec-modules",
78+
"description": "List all top-level modules (service folders) under azure-rest-api-specs/specification.",
79+
"parameters": [],
80+
"callbackName": "listSpecModules"
81+
},
82+
{
83+
"name": "list-providers",
84+
"description": "List provider namespaces for a given service under resource-manager.",
85+
"parameters": [
86+
{ "name": "service", "description": "Service folder name under specification (e.g., hybridconnectivity)", "type": "string" }
87+
],
88+
"callbackName": "listProvidersForService"
89+
},
90+
{
91+
"name": "list-api-versions",
92+
"description": "List available API versions for a given service and provider (stable/preview).",
93+
"parameters": [
94+
{ "name": "service", "description": "Service folder name under specification", "type": "string" },
95+
{ "name": "provider", "description": "Provider namespace folder under the service (e.g., Microsoft.HybridConnectivity)", "type": "string" }
96+
],
97+
"callbackName": "listApiVersions"
98+
},
99+
{
100+
"name": "resolve-autorest-inputs",
101+
"description": "Resolve the four Autorest inputs (serviceName, commitId, serviceSpecs, swaggerFileSpecs) from service/provider/version.",
102+
"parameters": [
103+
{ "name": "service", "description": "Service folder name under specification", "type": "string" },
104+
{ "name": "provider", "description": "Provider namespace under the service", "type": "string" },
105+
{ "name": "stability", "description": "'stable' or 'preview'", "type": "string" },
106+
{ "name": "version", "description": "API version (e.g., 2024-12-01)", "type": "string" }
107+
],
108+
"callbackName": "resolveAutorestInputs"
109+
}
75110
],
76111
"prompts": [
77112
{

0 commit comments

Comments
 (0)