Skip to main content
Glama

Azure Topology Graph MCP Server

by dhippley
server.ts18.3 kB
#!/usr/bin/env node const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } = require('@modelcontextprotocol/sdk/types.js'); const { z } = require('zod'); const { DefaultAzureCredential } = require('@azure/identity'); const { ResourceGraphClient } = require('@azure/arm-resourcegraph'); const { NetworkManagementClient } = require('@azure/arm-network'); const dotenv = require('dotenv'); // Load environment variables dotenv.config(); // Environment validation schema const envSchema = z.object({ AZURE_TENANT_ID: z.string().optional(), AZURE_CLIENT_ID: z.string().optional(), AZURE_CLIENT_SECRET: z.string().optional(), SUBSCRIPTION_IDS: z.string(), DEFAULT_RG: z.string().optional(), DEFAULT_REGION: z.string().optional(), NETWORK_WATCHER_NAME: z.string().optional(), }); // Validate environment variables const env = envSchema.parse(process.env); const subscriptionIds = env.SUBSCRIPTION_IDS.split(',').map((id: string) => id.trim()); // Azure clients let resourceGraphClient: any; let networkClients: Map<string, any> = new Map(); // Initialize Azure clients async function initializeClients() { try { const credential = new DefaultAzureCredential(); // Initialize Resource Graph client resourceGraphClient = new ResourceGraphClient(credential); // Initialize Network Management clients for each subscription for (const subscriptionId of subscriptionIds) { const networkClient = new NetworkManagementClient(credential, subscriptionId); networkClients.set(subscriptionId, networkClient); } console.error('Azure clients initialized successfully'); } catch (error) { console.error('Failed to initialize Azure clients:', error); throw error; } } // Graph data structures interface GraphNode { id: string; type: string; name: string; subscriptionId: string; resourceGroup: string; location: string; tags?: Record<string, string>; properties?: any; } interface GraphEdge { source: string; target: string; type: string; properties?: any; } interface TopologyGraph { nodes: GraphNode[]; edges: GraphEdge[]; } // Cache for topology data let topologyCache: TopologyGraph | null = null; let cacheTimestamp: number = 0; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes // Azure Resource Graph queries const RESOURCE_QUERY = ` resources | where subscriptionId in~ (${subscriptionIds.map((id: string) => `'${id}'`).join(',')}) | project id, type, name, subscriptionId, resourceGroup, location, tags, properties | order by name asc `; const NETWORK_RESOURCES_QUERY = ` resources | where subscriptionId in~ (${subscriptionIds.map((id: string) => `'${id}'`).join(',')}) | where type in~ ( 'microsoft.network/virtualnetworks', 'microsoft.network/subnets', 'microsoft.network/networkinterfaces', 'microsoft.network/publicipaddresses', 'microsoft.network/loadbalancers', 'microsoft.network/applicationgateways', 'microsoft.network/networksecuritygroups' ) | project id, type, name, subscriptionId, resourceGroup, location, tags, properties | order by name asc `; // Build topology from Azure resources async function buildTopology(): Promise<TopologyGraph> { const now = Date.now(); if (topologyCache && (now - cacheTimestamp) < CACHE_TTL) { return topologyCache; } console.error('Building topology from Azure resources...'); try { // Query all resources const resourcesResult = await resourceGraphClient.resources({ query: RESOURCE_QUERY, subscriptions: subscriptionIds, }); const nodes: GraphNode[] = []; const edges: GraphEdge[] = []; // Process resources into nodes if (resourcesResult.data) { for (const resource of resourcesResult.data as any[]) { const node: GraphNode = { id: resource.id, type: resource.type, name: resource.name, subscriptionId: resource.subscriptionId, resourceGroup: resource.resourceGroup, location: resource.location, tags: resource.tags, properties: resource.properties, }; nodes.push(node); } } // Build relationships/edges await buildResourceRelationships(nodes, edges); const topology: TopologyGraph = { nodes, edges }; topologyCache = topology; cacheTimestamp = now; console.error(`Topology built: ${nodes.length} nodes, ${edges.length} edges`); return topology; } catch (error) { console.error('Error building topology:', error); throw error; } } // Build relationships between resources async function buildResourceRelationships(nodes: GraphNode[], edges: GraphEdge[]) { const nodeMap = new Map(nodes.map(node => [node.id, node])); for (const node of nodes) { // Resource Group relationships const rgNodes = nodes.filter(n => n.resourceGroup === node.resourceGroup && n.id !== node.id); for (const rgNode of rgNodes) { if (!edges.some(e => e.source === node.id && e.target === rgNode.id && e.type === 'resource-group')) { edges.push({ source: node.id, target: rgNode.id, type: 'resource-group', }); } } // Network relationships if (node.type === 'microsoft.compute/virtualmachines') { await buildVMNetworkRelationships(node, nodes, edges); } else if (node.type === 'microsoft.network/virtualnetworks') { await buildVNetRelationships(node, nodes, edges); } } } // Build VM network relationships async function buildVMNetworkRelationships(vmNode: GraphNode, nodes: GraphNode[], edges: GraphEdge[]) { try { if (vmNode.properties?.networkProfile?.networkInterfaces) { for (const nicRef of vmNode.properties.networkProfile.networkInterfaces) { const nicNode = nodes.find(n => n.id === nicRef.id); if (nicNode) { edges.push({ source: vmNode.id, target: nicNode.id, type: 'network-interface', }); } } } } catch (error) { console.error(`Error building VM network relationships for ${vmNode.name}:`, error); } } // Build VNet relationships async function buildVNetRelationships(vnetNode: GraphNode, nodes: GraphNode[], edges: GraphEdge[]) { try { // Find subnets in the same resource group const subnets = nodes.filter(n => n.type === 'microsoft.network/virtualnetworks/subnets' && n.resourceGroup === vnetNode.resourceGroup && n.id.startsWith(vnetNode.id) ); for (const subnet of subnets) { edges.push({ source: vnetNode.id, target: subnet.id, type: 'subnet', }); } } catch (error) { console.error(`Error building VNet relationships for ${vnetNode.name}:`, error); } } // Search resources by query async function searchResources(query: string, resourceType?: string): Promise<GraphNode[]> { const topology = await buildTopology(); const searchLower = query.toLowerCase(); return topology.nodes.filter(node => { const matchesQuery = node.name.toLowerCase().includes(searchLower) || node.type.toLowerCase().includes(searchLower) || node.resourceGroup.toLowerCase().includes(searchLower) || node.location.toLowerCase().includes(searchLower) || (node.tags && Object.values(node.tags).some(tag => tag.toLowerCase().includes(searchLower) )); const matchesType = !resourceType || node.type.toLowerCase().includes(resourceType.toLowerCase()); return matchesQuery && matchesType; }); } // Get resource neighbors async function getResourceNeighbors(resourceId: string): Promise<{node: GraphNode, neighbors: GraphNode[]}> { const topology = await buildTopology(); const node = topology.nodes.find(n => n.id === resourceId); if (!node) { throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resourceId}`); } // Find all connected nodes const connectedIds = new Set<string>(); for (const edge of topology.edges) { if (edge.source === resourceId) { connectedIds.add(edge.target); } else if (edge.target === resourceId) { connectedIds.add(edge.source); } } const neighbors = topology.nodes.filter(n => connectedIds.has(n.id)); return { node, neighbors }; } // Find path between resources async function findResourcePath(sourceId: string, targetId: string): Promise<GraphNode[]> { const topology = await buildTopology(); if (!topology.nodes.find(n => n.id === sourceId)) { throw new McpError(ErrorCode.InvalidRequest, `Source resource not found: ${sourceId}`); } if (!topology.nodes.find(n => n.id === targetId)) { throw new McpError(ErrorCode.InvalidRequest, `Target resource not found: ${targetId}`); } // Simple BFS to find shortest path const queue: string[][] = [[sourceId]]; const visited = new Set<string>([sourceId]); while (queue.length > 0) { const path = queue.shift()!; const current = path[path.length - 1]; if (current === targetId) { return path.map(id => topology.nodes.find(n => n.id === id)!); } // Find neighbors for (const edge of topology.edges) { let next: string | null = null; if (edge.source === current && !visited.has(edge.target)) { next = edge.target; } else if (edge.target === current && !visited.has(edge.source)) { next = edge.source; } if (next) { visited.add(next); queue.push([...path, next]); } } } return []; // No path found } // Create MCP server const server = new Server( { name: 'azure-topology-graph', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_resources', description: 'Search Azure resources by name, type, or other properties', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (searches name, type, resource group, location, tags)', }, resourceType: { type: 'string', description: 'Optional filter by resource type', }, }, required: ['query'], }, }, { name: 'get_resource', description: 'Get detailed information about a specific Azure resource', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Full Azure resource ID', }, }, required: ['resourceId'], }, }, { name: 'get_neighbors', description: 'Get resources connected to a specific resource', inputSchema: { type: 'object', properties: { resourceId: { type: 'string', description: 'Full Azure resource ID', }, }, required: ['resourceId'], }, }, { name: 'find_path', description: 'Find connection path between two Azure resources', inputSchema: { type: 'object', properties: { sourceId: { type: 'string', description: 'Source resource ID', }, targetId: { type: 'string', description: 'Target resource ID', }, }, required: ['sourceId', 'targetId'], }, }, { name: 'export_topology', description: 'Export the complete topology graph', inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['json', 'summary'], description: 'Export format', default: 'summary', }, }, }, }, { name: 'refresh_topology', description: 'Refresh the topology cache by re-querying Azure', inputSchema: { type: 'object', properties: {}, }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_resources': { const { query, resourceType } = args as { query: string; resourceType?: string }; const results = await searchResources(query, resourceType); return { content: [ { type: 'text', text: `Found ${results.length} resources matching "${query}":\n\n` + results.map(r => `• ${r.name} (${r.type})\n Resource Group: ${r.resourceGroup}\n Location: ${r.location}\n ID: ${r.id}` ).join('\n\n'), }, ], }; } case 'get_resource': { const { resourceId } = args as { resourceId: string }; const topology = await buildTopology(); const resource = topology.nodes.find(n => n.id === resourceId); if (!resource) { throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${resourceId}`); } return { content: [ { type: 'text', text: `Resource Details:\n\n` + `Name: ${resource.name}\n` + `Type: ${resource.type}\n` + `Resource Group: ${resource.resourceGroup}\n` + `Location: ${resource.location}\n` + `Subscription: ${resource.subscriptionId}\n` + `ID: ${resource.id}\n\n` + (resource.tags ? `Tags: ${JSON.stringify(resource.tags, null, 2)}\n\n` : '') + (resource.properties ? `Properties: ${JSON.stringify(resource.properties, null, 2)}` : ''), }, ], }; } case 'get_neighbors': { const { resourceId } = args as { resourceId: string }; const { node, neighbors } = await getResourceNeighbors(resourceId); return { content: [ { type: 'text', text: `Resource: ${node.name} (${node.type})\n\n` + `Connected Resources (${neighbors.length}):\n\n` + neighbors.map(n => `• ${n.name} (${n.type})\n Resource Group: ${n.resourceGroup}\n Location: ${n.location}` ).join('\n\n'), }, ], }; } case 'find_path': { const { sourceId, targetId } = args as { sourceId: string; targetId: string }; const path = await findResourcePath(sourceId, targetId); if (path.length === 0) { return { content: [ { type: 'text', text: `No connection path found between the specified resources.`, }, ], }; } return { content: [ { type: 'text', text: `Connection Path (${path.length} hops):\n\n` + path.map((node, index) => `${index + 1}. ${node.name} (${node.type})\n ${node.id}` ).join('\n\n'), }, ], }; } case 'export_topology': { const { format = 'summary' } = args as { format?: 'json' | 'summary' }; const topology = await buildTopology(); if (format === 'json') { return { content: [ { type: 'text', text: JSON.stringify(topology, null, 2), }, ], }; } else { const resourceTypes = Array.from(new Set(topology.nodes.map(n => n.type))).sort(); const subscriptions = Array.from(new Set(topology.nodes.map(n => n.subscriptionId))).sort(); const resourceGroups = Array.from(new Set(topology.nodes.map(n => n.resourceGroup))).sort(); return { content: [ { type: 'text', text: `Azure Topology Summary:\n\n` + `Total Resources: ${topology.nodes.length}\n` + `Total Connections: ${topology.edges.length}\n\n` + `Subscriptions (${subscriptions.length}):\n${subscriptions.map(s => `• ${s}`).join('\n')}\n\n` + `Resource Groups (${resourceGroups.length}):\n${resourceGroups.map(rg => `• ${rg}`).join('\n')}\n\n` + `Resource Types (${resourceTypes.length}):\n${resourceTypes.map(rt => `• ${rt}`).join('\n')}`, }, ], }; } } case 'refresh_topology': { topologyCache = null; cacheTimestamp = 0; const topology = await buildTopology(); return { content: [ { type: 'text', text: `Topology refreshed successfully.\n\n` + `Resources: ${topology.nodes.length}\n` + `Connections: ${topology.edges.length}`, }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`); } }); // Start the server async function main() { try { await initializeClients(); const transport = new StdioServerTransport(); await server.connect(transport); console.error('Azure Topology Graph MCP Server started successfully'); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } if (require.main === module) { main().catch(console.error); }

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/dhippley/azure_mcp_graph'

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