Skip to main content
Glama
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 { AuthorizationManagementClient } from "@azure/arm-authorization"; import { ResourceManagementClient } from "@azure/arm-resources"; import { SubscriptionClient } from "@azure/arm-subscriptions"; import LoggerService from "./LoggerService"; import { parseEnvInt } from "./config"; // Constants const CONFIG = { SERVER_VERSION: process.env.SERVER_VERSION || "1.0.0", MAX_RETRIES: parseEnvInt(process.env.MAX_RETRIES || "3", 10), RETRY_DELAY_MS: parseEnvInt(process.env.RETRY_DELAY_MS || "1000", 10), LOG_LEVEL: process.env.LOG_LEVEL || "info", }; // Type definitions interface ServerContext { resourceClient: ResourceManagementClient | null; subscriptionClient: SubscriptionClient | null; authorizationClient: AuthorizationManagementClient | null; credentials: ChainedTokenCredential | null; selectedTenant: string | null; selectedSubscription: string | null; apiVersion?: string; } // Error classes class AzureMCPError extends Error { constructor(message: string, public readonly code: string) { super(message); this.name = "AzureMCPError"; } } class AzureAuthenticationError extends AzureMCPError { constructor(message: string) { super(message, "AUTH_FAILED"); } } class AzureResourceError extends AzureMCPError { constructor(message: string) { super(message, "RESOURCE_ERROR"); } } // 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, 'subscriptionClient' for SubscriptionClient, and 'authorizationClient' for AuthorizationManagementClient - 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; private resourceCache: Map<string, any> = new Map(); private azureOperations!: AzureOperations; constructor() { this.context = { selectedTenant: null, selectedSubscription: null, credentials: null, resourceClient: null, subscriptionClient: null, authorizationClient: null, }; this.server = new Server( { name: "azure-mcp", version: CONFIG.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) ); // Initialize Azure operations after setting up request handlers this.azureOperations = new AzureOperations(this.context, this.logger); } private createCredential(tenantId?: string): ChainedTokenCredential { const credentials = []; // Add environment-based credential if ( process.env.AZURE_CLIENT_ID && process.env.AZURE_CLIENT_SECRET && process.env.AZURE_TENANT_ID ) { credentials.push( new ClientSecretCredential( process.env.AZURE_TENANT_ID, process.env.AZURE_CLIENT_ID, process.env.AZURE_CLIENT_SECRET ) ); } // Add managed identity with specific client ID if available if (process.env.AZURE_CLIENT_ID) { credentials.push( new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID) ); } else { credentials.push(new ManagedIdentityCredential()); } // Add default Azure credential as fallback credentials.push( new DefaultAzureCredential({ tenantId: tenantId || process.env.AZURE_TENANT_ID, }) ); return new ChainedTokenCredential(...credentials); } private async initializeClients( tenantId: string, subscriptionId: string ): Promise<void> { try { // Use enhanced credential creation this.context.credentials = this.createCredential(tenantId); 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 ); // ADD THIS LINE - Initialize Authorization Client this.context.authorizationClient = new AuthorizationManagementClient( this.context.credentials, subscriptionId ); this.logWithContext( "info", `Clients initialized for tenant: ${tenantId} and subscription: ${subscriptionId}` ); } catch (error) { this.logWithContext("error", `Failed to initialize clients: ${error}`, { error, }); throw new AzureAuthenticationError( `Failed to initialize Azure clients: ${error}` ); } } private async getCachedResource( key: string, fetchFn: () => Promise<any>, ttlMs = 60000 ): Promise<any> { const cachedItem = this.resourceCache.get(key); if (cachedItem && Date.now() - cachedItem.timestamp < ttlMs) { return cachedItem.data; } const data = await fetchFn(); this.resourceCache.set(key, { data, timestamp: Date.now(), }); return data; } private logWithContext( level: string, message: string, context: Record<string, any> = {} ): void { const logEntry = { timestamp: new Date().toISOString(), level, message, tenant: this.context.selectedTenant, subscription: this.context.selectedSubscription, ...context, }; // Fix logger access by using type-safe methods switch (level) { case "info": this.logger.info(JSON.stringify(logEntry)); break; case "warning": case "warn": this.logger.warning(JSON.stringify(logEntry)); break; case "error": this.logger.error(JSON.stringify(logEntry)); break; default: this.logger.info(JSON.stringify(logEntry)); } } private async handleListTools() { return { tools: [ // Add these new tools to your handleListTools method, inside the tools array: { name: "list-role-assignments", description: "List role assignments for the subscription or resource group", inputSchema: { type: "object", properties: { scope: { type: "string", description: "Scope for role assignments (subscription, resource group, or resource ID). Leave empty for subscription level.", }, }, required: [], }, }, { name: "get-role-definitions", description: "List available role definitions", inputSchema: { type: "object", properties: { scope: { type: "string", description: "Scope for role definitions. Leave empty for subscription level.", }, }, required: [], }, }, { name: "get-user-permissions", description: "Get detailed user permissions by combining role assignments and role definitions", inputSchema: { type: "object", properties: { scope: { type: "string", description: "Scope to check permissions for. Leave empty for subscription level.", }, }, required: [], }, }, { 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"], }, }, // New tools { name: "list-resource-groups", description: "List all resource groups in the selected subscription", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get-resource-details", description: "Get detailed information about a specific resource", inputSchema: { type: "object", properties: { resourceId: { type: "string", description: "Azure Resource ID", }, }, required: ["resourceId"], }, }, { name: "create-resource-group", description: "Create a new resource group", inputSchema: { type: "object", properties: { name: { type: "string", description: "Resource group name", }, location: { type: "string", description: "Azure region", }, tags: { type: "object", description: "Resource tags (optional)", }, }, required: ["name", "location"], }, }, ], }; } private async executeWithRetry<T>( operation: () => Promise<T>, retries = CONFIG.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.logWithContext( "warning", `Retry ${i + 1}/${retries} failed: ${error}`, { error } ); if (i < retries - 1) { await new Promise((resolve) => setTimeout(resolve, CONFIG.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; // New tools case "list-resource-groups": result = await this.handleListResourceGroups(); break; case "get-resource-details": result = await this.handleGetResourceDetails(args); break; case "create-resource-group": result = await this.handleCreateResourceGroup(args); break; case "list-role-assignments": result = await this.handleListRoleAssignments(args); break; case "get-role-definitions": result = await this.handleGetRoleDefinitions(args); break; case "get-user-permissions": result = await this.handleGetUserPermissions(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.logWithContext("error", `Error in handleCallTool: ${error}`, { 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 { 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.logWithContext("error", `Error executing user code: ${error}`, { 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.logWithContext("error", `Error listing tenants: ${error}`, { error, }); throw new AzureAuthenticationError( `Failed to list tenants and subscriptions: ${error}` ); } } 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 async handleListResourceGroups() { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } try { const cacheKey = `resource-groups-${this.context.selectedSubscription}`; return await this.getCachedResource( cacheKey, async () => { // Use azureOperations to handle the business logic return await this.azureOperations.listResourceGroups(); }, 30000 ); } catch (error) { this.logWithContext("error", `Error listing resource groups: ${error}`, { error, }); throw new AzureResourceError(`Failed to list resource groups: ${error}`); } } private async handleGetResourceDetails(args: any) { const { resourceId } = z .object({ resourceId: z.string().min(1, "Resource ID cannot be empty"), }) .parse(args); if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } try { // The resource ID format is: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{provider}/{resourceType}/{resourceName} const parts = resourceId.split("/"); if (parts.length < 8) { throw new AzureResourceError("Invalid resource ID format"); } const cacheKey = `resource-${resourceId}`; const resource = await this.getCachedResource( cacheKey, async () => { // Use azureOperations to get the resource return await this.azureOperations.getResource(resourceId); }, 60000 ); // Cache for 1 minute return { id: resource.id, name: resource.name, type: resource.type, location: resource.location, tags: resource.tags || {}, properties: resource.properties || {}, }; } catch (error) { this.logWithContext("error", `Error getting resource details: ${error}`, { error, }); throw new AzureResourceError(`Failed to get resource details: ${error}`); } } private async handleCreateResourceGroup(args: any) { const { name, location, tags } = z .object({ name: z.string().min(1, "Resource group name cannot be empty"), location: z.string().min(1, "Location cannot be empty"), tags: z.record(z.string()).optional(), }) .parse(args); if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } try { // Use azureOperations to create the resource group const result = await this.azureOperations.createResourceGroup( name, location, tags ); // Invalidate cache for resource groups list this.resourceCache.delete( `resource-groups-${this.context.selectedSubscription}` ); return { id: result.id, name: result.name, location: result.location, tags: result.tags || {}, provisioningState: result.properties?.provisioningState, }; } catch (error) { this.logWithContext("error", `Error creating resource group: ${error}`, { error, }); throw new AzureResourceError(`Failed to create resource group: ${error}`); } } private async handleListRoleAssignments(args: any) { const { scope } = z .object({ scope: z.string().optional(), }) .parse(args); if (!this.context.authorizationClient) { throw new AzureMCPError( "Authorization client not initialized", "NO_CLIENT" ); } try { const roleAssignments = []; const assignmentScope = scope || `/subscriptions/${this.context.selectedSubscription}`; for await (const assignment of this.context.authorizationClient.roleAssignments.listForScope( assignmentScope )) { roleAssignments.push({ id: assignment.id, principalId: assignment.principalId, principalType: assignment.principalType, roleDefinitionId: assignment.roleDefinitionId, scope: assignment.scope, createdOn: assignment.createdOn, createdBy: assignment.createdBy, }); } return { roleAssignments, total: roleAssignments.length }; } catch (error) { this.logWithContext("error", `Error listing role assignments: ${error}`, { error, }); throw new AzureResourceError(`Failed to list role assignments: ${error}`); } } private async handleGetRoleDefinitions(args: any) { const { scope } = z .object({ scope: z.string().optional(), }) .parse(args); if (!this.context.authorizationClient) { throw new AzureMCPError( "Authorization client not initialized", "NO_CLIENT" ); } try { const roleDefinitions = []; const definitionScope = scope || `/subscriptions/${this.context.selectedSubscription}`; for await (const definition of this.context.authorizationClient.roleDefinitions.list( definitionScope )) { roleDefinitions.push({ id: definition.id, name: definition.name, roleName: definition.roleName, description: definition.description, type: definition.type, permissions: definition.permissions?.map((p) => ({ actions: p.actions, notActions: p.notActions, dataActions: p.dataActions, notDataActions: p.notDataActions, })), }); } return { roleDefinitions, total: roleDefinitions.length }; } catch (error) { this.logWithContext("error", `Error getting role definitions: ${error}`, { error, }); throw new AzureResourceError(`Failed to get role definitions: ${error}`); } } private async handleGetUserPermissions(args: any) { const { scope } = z .object({ scope: z.string().optional(), }) .parse(args); if (!this.context.authorizationClient) { throw new AzureMCPError( "Authorization client not initialized", "NO_CLIENT" ); } try { const permissionScope = scope || `/subscriptions/${this.context.selectedSubscription}`; // Get both role assignments and role definitions const [roleAssignments, roleDefinitions] = await Promise.all([ this.getRoleAssignments(permissionScope), this.getRoleDefinitions(permissionScope), ]); // Match assignments with definitions const userPermissions = roleAssignments.map((assignment) => { const roleDefinition = roleDefinitions.find((def) => assignment.roleDefinitionId?.endsWith(def.name || "") ); return { principalId: assignment.principalId, principalType: assignment.principalType, scope: assignment.scope, roleDefinition: { id: roleDefinition?.id, name: roleDefinition?.roleName, description: roleDefinition?.description, permissions: roleDefinition?.permissions || [], }, createdOn: assignment.createdOn, }; }); // Group by role for summary const roleSummary = userPermissions.reduce((acc, perm) => { const roleName = perm.roleDefinition.name || "Unknown"; acc[roleName] = (acc[roleName] || 0) + 1; return acc; }, {} as Record<string, number>); return { userPermissions, roleSummary, totalAssignments: roleAssignments.length, scope: permissionScope, }; } catch (error) { this.logWithContext("error", `Error getting user permissions: ${error}`, { error, }); throw new AzureResourceError(`Failed to get user permissions: ${error}`); } } private async getRoleAssignments(scope: string) { const assignments = []; for await (const assignment of this.context.authorizationClient!.roleAssignments.listForScope( scope )) { assignments.push(assignment); } return assignments; } private async getRoleDefinitions(scope: string) { const definitions = []; for await (const definition of this.context.authorizationClient!.roleDefinitions.list( scope )) { definitions.push(definition); } return definitions; } private wrapUserCode(userCode: string): string { try { // Sanitize user code to prevent certain patterns const sanitizedCode = userCode .replace(/process\.env/g, "/* process.env access blocked */") .replace(/require\s*\(/g, "/* require blocked */") .replace(/import\s+.*\s+from/g, "/* import blocked */"); const project = new Project({ useInMemoryFileSystem: true, }); const sourceFile = project.createSourceFile("userCode.ts", sanitizedCode); 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.logWithContext("error", `Error wrapping user code: ${error}`, { 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.logWithContext("info", "Azure MCP Server running on stdio"); } catch (error) { this.logWithContext("error", `Failed to start server: ${error}`, { error, }); throw new AzureMCPError("Failed to start server", "START_FAILED"); } } // For testing purposes only public async __testOnly_setContext(context: Partial<ServerContext>) { this.context = { ...this.context, ...context }; return "Context updated for testing"; } } // Azure Operations class for better separation of concerns class AzureOperations { constructor(private context: ServerContext, private logger: any) {} async listResourceGroups() { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } const resourceGroups = []; for await (const group of this.context.resourceClient.resourceGroups.list()) { resourceGroups.push({ id: group.id, name: group.name, location: group.location, tags: group.tags || {}, }); } return resourceGroups; } async getResource(resourceId: string) { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } return await this.context.resourceClient.resources.getById( resourceId, "latest" ); } async createResourceGroup( name: string, location: string, tags?: Record<string, string> ) { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } return await this.context.resourceClient.resourceGroups.createOrUpdate( name, { location, tags } ); } async listResourcesByType(resourceType: string, provider: string) { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } const resources = []; // Using list() with a filter instead of listByResourceType which doesn't exist const filter = `resourceType eq '${provider}/${resourceType}'`; for await (const resource of this.context.resourceClient.resources.list({ filter, })) { resources.push({ id: resource.id, name: resource.name, type: resource.type, location: resource.location, tags: resource.tags || {}, }); } return resources; } async getResourceGroup(resourceGroupName: string) { if (!this.context.resourceClient) { throw new AzureMCPError("Client not initialized", "NO_CLIENT"); } return await this.context.resourceClient.resourceGroups.get( resourceGroupName ); } // Add these methods to your AzureOperations class: async listRoleAssignments(scope?: string) { if (!this.context.authorizationClient) { throw new AzureMCPError( "Authorization client not initialized", "NO_CLIENT" ); } const assignments = []; const assignmentScope = scope || `/subscriptions/${this.context.selectedSubscription}`; for await (const assignment of this.context.authorizationClient.roleAssignments.listForScope( assignmentScope )) { assignments.push({ id: assignment.id, principalId: assignment.principalId, principalType: assignment.principalType, roleDefinitionId: assignment.roleDefinitionId, scope: assignment.scope, }); } return assignments; } async getRoleDefinitions(scope?: string) { if (!this.context.authorizationClient) { throw new AzureMCPError( "Authorization client not initialized", "NO_CLIENT" ); } const definitions = []; const definitionScope = scope || `/subscriptions/${this.context.selectedSubscription}`; for await (const definition of this.context.authorizationClient.roleDefinitions.list( definitionScope )) { definitions.push({ id: definition.id, name: definition.name, roleName: definition.roleName, description: definition.description, permissions: definition.permissions, }); } return definitions; } } // Schema definitions const RunAzureCodeSchema = z.object({ reasoning: z .string() .min(1, "Reasoning cannot be empty") .describe("The reasoning behind the code"), code: z.string().min(1, "Code cannot be empty").describe(codePrompt), tenantId: z.string().optional().describe("Azure Tenant ID"), subscriptionId: z.string().optional().describe("Azure Subscription ID"), }); const SelectTenantSchema = z.object({ tenantId: z.string().describe("Azure Tenant ID to select"), subscriptionId: z.string().describe("Azure Subscription ID to select"), }); // 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 };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kalivaraprasad-gonapa/azure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server