Azure MCP Server

Official
by Streen9
Verified
  • src
import { Project, SyntaxKind } from "ts-morph"; import { createContext, runInContext } from "node:vm"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { DefaultAzureCredential, ClientSecretCredential, ChainedTokenCredential, ManagedIdentityCredential } from "@azure/identity"; import { ResourceManagementClient } from "@azure/arm-resources"; import { SubscriptionClient } from "@azure/arm-subscriptions"; import LoggerService from "./LoggerService"; // Constants const SERVER_VERSION = "1.0.0"; const MAX_RETRIES = 3; const RETRY_DELAY_MS = 1000; // Type definitions interface ServerContext { resourceClient: ResourceManagementClient | null; subscriptionClient: SubscriptionClient | null; credentials: ChainedTokenCredential | null; selectedTenant: string | null; selectedSubscription: string | null; } // Error classes class AzureMCPError extends Error { constructor(message: string, public readonly code: string) { super(message); this.name = 'AzureMCPError'; } } // Code prompt template const codePrompt = `Your job is to answer questions about Azure environment by writing Javascript code using Azure SDK. The code must adhere to a few rules: - Use the provided client instances: 'resourceClient' for ResourceManagementClient and 'subscriptionClient' for SubscriptionClient - DO NOT create new client instances or import Azure SDK packages - Use async/await and promises - Think step-by-step before writing the code - Avoid hardcoded values like Resource IDs - Handle errors gracefully - Handle pagination correctly using for-await-of loops - Data returned must be JSON containing only the minimal amount of data needed - Code MUST "return" a value: string, number, boolean or JSON object`; class AzureMCPServer { private server: Server; private context: ServerContext; private transport: StdioServerTransport; private logger = LoggerService; constructor() { this.context = { selectedTenant: null, selectedSubscription: null, credentials: null, resourceClient: null, subscriptionClient: null }; this.server = new Server( { name: "azure-mcp", version: SERVER_VERSION, }, { capabilities: { tools: {}, }, } ); this.transport = new StdioServerTransport(); this.initializeRequestHandlers(); } private initializeRequestHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, this.handleListTools.bind(this)); this.server.setRequestHandler(CallToolRequestSchema, this.handleCallTool.bind(this)); } private async initializeClients(tenantId: string, subscriptionId: string): Promise<void> { try { // Use DefaultAzureCredential which will automatically try different authentication methods // This includes environment variables, managed identity, Azure CLI, etc. this.context.credentials = new DefaultAzureCredential(); this.context.selectedTenant = tenantId; this.context.selectedSubscription = subscriptionId; this.context.resourceClient = new ResourceManagementClient( this.context.credentials, subscriptionId ); this.context.subscriptionClient = new SubscriptionClient( this.context.credentials ); this.logger.info(`Clients initialized for tenant: ${tenantId} and subscription: ${subscriptionId}`); } catch (error) { this.logger.error(`Failed to initialize clients: ${error}`); throw new AzureMCPError( "Failed to initialize Azure clients", "INIT_FAILED" ); } } private async handleListTools() { return { tools: [ { name: "run-azure-code", description: "Run Azure code", inputSchema: { type: "object", properties: { reasoning: { type: "string", description: "The reasoning behind the code", }, code: { type: "string", description: codePrompt, }, tenantId: { type: "string", description: "Azure Tenant ID", }, subscriptionId: { type: "string", description: "Azure Subscription ID", } }, required: ["reasoning", "code"], }, }, { name: "list-tenants", description: "List all available Azure tenants", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "select-tenant", description: "Select Azure tenant and subscription", inputSchema: { type: "object", properties: { tenantId: { type: "string", description: "Azure Tenant ID to select", }, subscriptionId: { type: "string", description: "Azure Subscription ID to select", }, }, required: ["tenantId", "subscriptionId"], }, }, ], }; } private async executeWithRetry<T>( operation: () => Promise<T>, retries = MAX_RETRIES ): Promise<T> { let lastError: Error | null = null; for (let i = 0; i < retries; i++) { try { return await operation(); } catch (error) { lastError = error as Error; this.logger.warning(`Retry ${i + 1}/${retries} failed: ${error}`); if (i < retries - 1) { await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1))); } } } throw lastError || new Error('Operation failed after retries'); } private async handleCallTool(request: z.infer<typeof CallToolRequestSchema>) { const { name, arguments: args } = request.params; try { let result; switch (name) { case "run-azure-code": result = await this.handleRunAzureCode(args); break; case "list-tenants": result = await this.handleListTenants(); break; case "select-tenant": result = await this.handleSelectTenant(args); break; default: throw new AzureMCPError(`Unknown tool: ${name}`, "UNKNOWN_TOOL"); } // Ensure the result is properly formatted before returning return this.createTextResponse( typeof result === 'string' ? result : JSON.stringify(result) ); } catch (error) { this.logger.error(`Error in handleCallTool: ${error}`); if (error instanceof z.ZodError) { throw new AzureMCPError( `Invalid arguments: ${error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) .join(", ")}`, "INVALID_ARGS" ); } // Ensure errors are properly formatted as well return this.createTextResponse( JSON.stringify({ error: error instanceof Error ? error.message : String(error), code: error instanceof AzureMCPError ? error.code : "UNKNOWN_ERROR" }) ); } } private async handleRunAzureCode(args: any) { const { reasoning, code, tenantId, subscriptionId } = RunAzureCodeSchema.parse(args); if (!this.context.selectedTenant && !tenantId) { throw new AzureMCPError( "Please select a tenant first using the 'select-tenant' tool!", "NO_TENANT" ); } if (tenantId && subscriptionId) { await this.initializeClients(tenantId, subscriptionId); } if (!this.context.resourceClient || !this.context.subscriptionClient) { throw new AzureMCPError( "Clients not initialized", "NO_CLIENTS" ); } const wrappedCode = this.wrapUserCode(code); const wrappedIIFECode = `(async function() { return (async () => { ${wrappedCode} })(); })()`; try { const result = await this.executeWithRetry(() => runInContext(wrappedIIFECode, createContext(this.context)) ); return this.createTextResponse(JSON.stringify(result)); } catch (error) { this.logger.error(`Error executing user code: ${error}`); throw new AzureMCPError( `Failed to execute code: ${error}`, "CODE_EXECUTION_FAILED" ); } } private async handleListTenants() { try { const creds = new DefaultAzureCredential(); const client = new SubscriptionClient(creds); const [tenants, subscriptions] = await Promise.all([ this.executeWithRetry(async () => { const items = []; for await (const tenant of client.tenants.list()) { items.push({ id: tenant.tenantId, name: tenant.displayName }); } return items; }), this.executeWithRetry(async () => { const items = []; for await (const sub of client.subscriptions.list()) { items.push({ id: sub.subscriptionId, name: sub.displayName, state: sub.state }); } return items; }) ]); return this.createTextResponse(JSON.stringify({ tenants, subscriptions })); } catch (error) { this.logger.error(`Error listing tenants: ${error}`); throw new AzureMCPError( "Failed to list tenants and subscriptions", "LIST_FAILED" ); } } private async handleSelectTenant(args: any) { const { tenantId, subscriptionId } = SelectTenantSchema.parse(args); await this.initializeClients(tenantId, subscriptionId); return this.createTextResponse("Tenant and subscription selected! Clients initialized."); } private wrapUserCode(userCode: string): string { try { const project = new Project({ useInMemoryFileSystem: true, }); const sourceFile = project.createSourceFile("userCode.ts", userCode); const lastStatement = sourceFile.getStatements().pop(); if (lastStatement && lastStatement.getKind() === SyntaxKind.ExpressionStatement) { const returnStatement = lastStatement.asKind(SyntaxKind.ExpressionStatement); if (returnStatement) { const expression = returnStatement.getExpression(); sourceFile.addStatements(`return ${expression.getText()};`); returnStatement.remove(); } } return sourceFile.getFullText(); } catch (error) { this.logger.error(`Error wrapping user code: ${error}`); throw new AzureMCPError( "Failed to process user code", "CODE_WRAP_FAILED" ); } } private createTextResponse(text: string) { try { // If the input is already a JSON string, parse and reconstruct it properly const parsed = JSON.parse(text); return { content: [{ type: "text", text: JSON.stringify(parsed) }] }; } catch { // If it's not valid JSON, clean up the string and format it properly const cleanText = text // Remove ANSI escape codes .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') // Remove log level indicators .replace(/\[info\]|\[error\]|\[warn\]/g, '') // Remove any potential HTML/XML-like tags .replace(/<[^>]*>/g, '') // Clean up extra whitespace .replace(/\s+/g, ' ') .trim(); // Ensure we're returning a valid MCP response format return { content: [{ type: "text", text: cleanText }] }; } } public async start(): Promise<void> { try { await this.server.connect(this.transport); this.logger.info("Azure MCP Server running on stdio"); } catch (error) { this.logger.error(`Failed to start server: ${error}`); throw new AzureMCPError( "Failed to start server", "START_FAILED" ); } } } // Schema definitions const RunAzureCodeSchema = z.object({ reasoning: z.string(), code: z.string(), tenantId: z.string().optional(), subscriptionId: z.string().optional(), }); const SelectTenantSchema = z.object({ tenantId: z.string(), subscriptionId: z.string(), }); // Start the server if (require.main === module) { const server = new AzureMCPServer(); server.start().catch((error) => { LoggerService.error(`Server failed to start: ${error}`); process.exit(1); }); } export { AzureMCPServer, AzureMCPError };