@north7/entraaware 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/build/index.js +356 -73
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -36,7 +36,7 @@ Create a `.mcp.json` file in your VS Code workspace:
36
36
  "command": "npx",
37
37
  "args": [
38
38
  "-y",
39
- "@uniquk/entraaware@latest"
39
+ "@north7/entraaware@latest"
40
40
  ],
41
41
  "env": {
42
42
  "TENANT_ID": "your-tenant-id",
package/build/index.js CHANGED
@@ -3,105 +3,388 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { Client } from "@microsoft/microsoft-graph-client";
6
- import { ClientSecretCredential } from "@azure/identity";
6
+ import { ClientSecretCredential, DefaultAzureCredential } from "@azure/identity";
7
7
  import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index.js";
8
- // Initialize Graph Client
8
+ // Initialize clients
9
9
  let graphClient = null;
10
+ let azureCredential = null;
10
11
  // Create server instance
11
12
  const server = new McpServer({
12
13
  name: "EntraAware",
13
- version: "0.0.1",
14
+ version: "0.0.3",
14
15
  capabilities: {
15
16
  resources: {},
16
17
  tools: {},
17
18
  },
18
19
  });
19
- // Direct Microsoft Graph API access tool
20
- server.tool("askEntra", "Ask any question about your Microsoft Entra (Azure AD) tenant in natural language", {
21
- question: z.string().describe("Your natural language question about Microsoft 365 Entra (Azure AD)")
22
- }, async ({ question }) => {
20
+ // SHARED UTILITIES
21
+ function getCredentials() {
22
+ const tenantId = process.env.TENANT_ID;
23
+ const clientId = process.env.CLIENT_ID;
24
+ const clientSecret = process.env.CLIENT_SECRET;
25
+ if (!tenantId || !clientId || !clientSecret) {
26
+ throw new Error("Missing required environment variables: TENANT_ID, CLIENT_ID, or CLIENT_SECRET");
27
+ }
28
+ return { tenantId, clientId, clientSecret };
29
+ }
30
+ function getAzureCredential() {
31
+ if (!azureCredential) {
32
+ try {
33
+ // First try to create a DefaultAzureCredential that can use Azure CLI or other authentication methods
34
+ azureCredential = new DefaultAzureCredential();
35
+ console.error("Using DefaultAzureCredential - will try Azure CLI if environment variables not set");
36
+ }
37
+ catch (error) {
38
+ // Fall back to ClientSecretCredential if DefaultAzureCredential fails
39
+ console.error("DefaultAzureCredential failed, falling back to ClientSecretCredential");
40
+ const { tenantId, clientId, clientSecret } = getCredentials();
41
+ azureCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
42
+ }
43
+ }
44
+ return azureCredential;
45
+ }
46
+ function formatApiResponse(apiType, method, path, result) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `${apiType} API Result (${method.toUpperCase()} ${path}):\n\n${JSON.stringify(result, null, 2)}`,
52
+ },
53
+ ],
54
+ };
55
+ }
56
+ function formatErrorResponse(err, apiType) {
57
+ const errorDetail = err instanceof Error
58
+ ? {
59
+ message: err.message,
60
+ name: err.name,
61
+ detail: apiType === 'Entra'
62
+ ? err.body ? JSON.stringify(err.body) : undefined
63
+ : err.response?.body || undefined,
64
+ status: err.statusCode || err.status
65
+ }
66
+ : String(err);
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: `Error querying ${apiType} API: ${JSON.stringify(errorDetail, null, 2)}`,
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ // MICROSOFT GRAPH API TOOL
77
+ server.tool("askEntra", "Direct access to Microsoft Graph API for accurate Entra (Azure AD) data", {
78
+ path: z.string().describe("The Graph API URL path (e.g. '/users/{id}/memberOf', '/directoryRoles')"),
79
+ method: z.enum(["get", "post", "put", "patch", "delete"]).default("get").describe("HTTP method to use"),
80
+ queryParams: z.record(z.string()).optional().describe("Query parameters for the request"),
81
+ body: z.record(z.string(), z.any()).optional().describe("Request body for POST/PUT/PATCH requests"),
82
+ apiVersion: z.enum(["v1.0", "beta"]).default("v1.0").describe("Microsoft Graph API version"),
83
+ fetchAllPages: z.boolean().optional().default(false).describe("Automatically fetch all pages of results"),
84
+ consistencyLevel: z.string().optional().describe("ConsistencyLevel header value (use 'eventual' for queries with $filter, $search, etc.)"),
85
+ // Shorthand params
86
+ select: z.string().optional().describe("Shorthand for $select query parameter"),
87
+ filter: z.string().optional().describe("Shorthand for $filter query parameter"),
88
+ expand: z.string().optional().describe("Shorthand for $expand query parameter"),
89
+ orderBy: z.string().optional().describe("Shorthand for $orderBy query parameter"),
90
+ top: z.number().optional().describe("Shorthand for $top query parameter"),
91
+ count: z.boolean().optional().describe("Shorthand for $count=true to include count of items"),
92
+ }, async ({ path, method, queryParams = {}, body, apiVersion, fetchAllPages, consistencyLevel, select, filter, expand, orderBy, top, count }) => {
23
93
  try {
24
- // Get or initialize Graph client
94
+ // Process shorthand query parameters
95
+ const processedParams = { ...queryParams };
96
+ if (select)
97
+ processedParams['$select'] = select;
98
+ if (filter)
99
+ processedParams['$filter'] = filter;
100
+ if (expand)
101
+ processedParams['$expand'] = expand;
102
+ if (orderBy)
103
+ processedParams['$orderby'] = orderBy;
104
+ if (top !== undefined)
105
+ processedParams['$top'] = top.toString();
106
+ if (count)
107
+ processedParams['$count'] = 'true';
108
+ // Initialize or get Graph client
25
109
  if (!graphClient) {
26
- graphClient = initGraphClient();
110
+ const credential = getAzureCredential();
111
+ const authProvider = new TokenCredentialAuthenticationProvider(credential, {
112
+ scopes: ["https://graph.microsoft.com/.default"],
113
+ });
114
+ graphClient = Client.initWithMiddleware({ authProvider });
115
+ }
116
+ // Build request with API path and version
117
+ let request = graphClient.api(path).version(apiVersion);
118
+ // Add query parameters
119
+ if (Object.keys(processedParams).length > 0) {
120
+ request = request.query(processedParams);
27
121
  }
28
- // Default path if we can't determine from the question
29
- let path = "/organization";
30
- // Extract any email addresses or GUIDs from the question
31
- const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/;
32
- const guidPattern = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
33
- const emailMatch = question.match(emailPattern);
34
- const guidMatch = question.match(guidPattern);
35
- // Check for specific keywords in the question to determine the right path
36
- const lowerQuestion = question.toLowerCase();
37
- if (lowerQuestion.includes("conditional access") || lowerQuestion.includes("policies")) {
38
- path = "/identity/conditionalAccess/policies";
122
+ // Add consistency level header if provided
123
+ if (consistencyLevel) {
124
+ request = request.header('ConsistencyLevel', consistencyLevel);
39
125
  }
40
- else if (lowerQuestion.includes("users") || lowerQuestion.includes("user")) {
41
- path = "/users";
42
- if (emailMatch) {
43
- path = `/users/${emailMatch[0]}`;
126
+ // Handle pagination for GET requests
127
+ let result;
128
+ if (method === 'get' && fetchAllPages) {
129
+ const firstPage = await request.get();
130
+ // If no pagination needed, return first page
131
+ if (!firstPage["@odata.nextLink"]) {
132
+ result = firstPage;
133
+ }
134
+ else {
135
+ // Collect all items from all pages
136
+ const allItems = [...(firstPage.value || [])];
137
+ let nextLink = firstPage["@odata.nextLink"];
138
+ while (nextLink) {
139
+ const nextPage = await graphClient.api(nextLink).get();
140
+ if (nextPage.value)
141
+ allItems.push(...nextPage.value);
142
+ nextLink = nextPage["@odata.nextLink"] || null;
143
+ }
144
+ result = {
145
+ "@odata.context": firstPage["@odata.context"],
146
+ value: allItems,
147
+ "@odata.count": firstPage["@odata.count"],
148
+ totalItemsFetched: allItems.length
149
+ };
44
150
  }
45
151
  }
46
- else if (lowerQuestion.includes("groups") || lowerQuestion.includes("group")) {
47
- path = "/groups";
152
+ else {
153
+ // Execute appropriate method
154
+ switch (method) {
155
+ case 'get':
156
+ result = await request.get();
157
+ break;
158
+ case 'post':
159
+ result = await request.post(body || {});
160
+ break;
161
+ case 'put':
162
+ result = await request.put(body || {});
163
+ break;
164
+ case 'patch':
165
+ result = await request.patch(body || {});
166
+ break;
167
+ case 'delete':
168
+ await request.delete();
169
+ result = { status: "Successfully deleted" };
170
+ break;
171
+ }
48
172
  }
49
- else if (lowerQuestion.includes("applications") || lowerQuestion.includes("apps")) {
50
- path = "/applications";
173
+ return formatApiResponse('Entra', method, path, result);
174
+ }
175
+ catch (err) {
176
+ return formatErrorResponse(err, 'Entra');
177
+ }
178
+ });
179
+ // AZURE RESOURCE MANAGEMENT API TOOL
180
+ server.tool("askAzure", "Direct access to Azure Resource Management API for managing Azure resources", {
181
+ path: z.string().describe("The Azure API path (e.g. '/subscriptions', '/resourceGroups/{name}')"),
182
+ method: z.enum(["get", "post", "put", "patch", "delete"]).default("get").describe("HTTP method to use"),
183
+ apiVersion: z.string().optional().describe("Azure API version - required for each Azure Resource Provider"),
184
+ subscriptionId: z.string().optional().describe("Azure Subscription ID (if not included in path)"),
185
+ body: z.record(z.string(), z.any()).optional().describe("Request body for POST/PUT/PATCH requests"),
186
+ queryParams: z.record(z.string()).optional().describe("Additional query parameters"),
187
+ fetchAllPages: z.boolean().optional().default(false).describe("Automatically fetch all pages of results"),
188
+ // Predefined operations
189
+ operation: z.enum([
190
+ "listResources", "listResourceProviders", "getResourceProvider",
191
+ "registerResourceProvider", "getResourceTypes", "getApiVersions",
192
+ "getLocations", "createResource", "deployTemplate", "deleteResource", "custom"
193
+ ]).optional().default("custom").describe("Predefined Azure operations"),
194
+ // Parameters for predefined operations
195
+ providerNamespace: z.string().optional().describe("Resource provider namespace (e.g. 'Microsoft.Compute')"),
196
+ resourceType: z.string().optional().describe("Resource type for specific operations"),
197
+ resourceGroupName: z.string().optional().describe("Resource group name for resource operations"),
198
+ resourceName: z.string().optional().describe("Resource name for resource operations"),
199
+ }, async ({ path, method, apiVersion, subscriptionId, body, queryParams = {}, fetchAllPages, operation = "custom", providerNamespace, resourceType, resourceGroupName, resourceName }) => {
200
+ try {
201
+ // Default API versions for common resource types
202
+ const defaultApiVersions = {
203
+ 'resources': '2021-04-01',
204
+ 'resourceGroups': '2021-04-01',
205
+ 'subscriptions': '2021-01-01',
206
+ 'providers': '2021-04-01',
207
+ 'deployments': '2021-04-01',
208
+ 'Microsoft.Compute/virtualMachines': '2023-03-01',
209
+ 'Microsoft.Storage/storageAccounts': '2023-01-01',
210
+ 'Microsoft.Network/virtualNetworks': '2023-04-01',
211
+ 'Microsoft.KeyVault/vaults': '2023-02-01'
212
+ };
213
+ // Handle predefined operations
214
+ if (operation !== "custom") {
215
+ const requiredSubscriptionId = !['listResourceProviders', 'getResourceProvider', 'registerResourceProvider'].includes(operation);
216
+ if (requiredSubscriptionId && !subscriptionId) {
217
+ throw new Error(`Operation '${operation}' requires a subscriptionId`);
218
+ }
219
+ switch (operation) {
220
+ case 'listResources':
221
+ path = resourceGroupName
222
+ ? `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/resources`
223
+ : `/subscriptions/${subscriptionId}/resources`;
224
+ apiVersion = apiVersion || defaultApiVersions['resources'];
225
+ break;
226
+ case 'listResourceProviders':
227
+ path = `/subscriptions/${subscriptionId}/providers`;
228
+ apiVersion = apiVersion || defaultApiVersions['providers'];
229
+ break;
230
+ case 'getResourceProvider':
231
+ if (!providerNamespace)
232
+ throw new Error("Operation 'getResourceProvider' requires a providerNamespace");
233
+ path = `/subscriptions/${subscriptionId}/providers/${providerNamespace}`;
234
+ apiVersion = apiVersion || defaultApiVersions['providers'];
235
+ break;
236
+ case 'registerResourceProvider':
237
+ if (!providerNamespace)
238
+ throw new Error("Operation 'registerResourceProvider' requires a providerNamespace");
239
+ path = `/subscriptions/${subscriptionId}/providers/${providerNamespace}/register`;
240
+ method = 'post';
241
+ apiVersion = apiVersion || defaultApiVersions['providers'];
242
+ break;
243
+ case 'getResourceTypes':
244
+ case 'getApiVersions':
245
+ case 'getLocations':
246
+ if (!providerNamespace)
247
+ throw new Error(`Operation '${operation}' requires providerNamespace`);
248
+ path = `/subscriptions/${subscriptionId}/providers/${providerNamespace}`;
249
+ apiVersion = apiVersion || defaultApiVersions['providers'];
250
+ break;
251
+ case 'createResource':
252
+ if (!resourceGroupName || !providerNamespace || !resourceType || !resourceName) {
253
+ throw new Error("Operation 'createResource' requires resourceGroupName, providerNamespace, resourceType, and resourceName");
254
+ }
255
+ if (!body)
256
+ throw new Error("Operation 'createResource' requires a request body");
257
+ path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${providerNamespace}/${resourceType}/${resourceName}`;
258
+ method = 'put';
259
+ const providerResourceKey = `${providerNamespace}/${resourceType}`;
260
+ apiVersion = apiVersion || defaultApiVersions[providerResourceKey] || '2021-04-01';
261
+ break;
262
+ case 'deployTemplate':
263
+ if (!resourceGroupName)
264
+ throw new Error("Operation 'deployTemplate' requires resourceGroupName");
265
+ if (!body?.properties?.template)
266
+ throw new Error("Operation 'deployTemplate' requires a template in the body");
267
+ const deploymentName = body.deploymentName || `deployment-${new Date().getTime()}`;
268
+ delete body.deploymentName;
269
+ path = `/subscriptions/${subscriptionId}/resourcegroups/${resourceGroupName}/providers/Microsoft.Resources/deployments/${deploymentName}`;
270
+ method = 'put';
271
+ apiVersion = apiVersion || defaultApiVersions['deployments'];
272
+ break;
273
+ case 'deleteResource':
274
+ if (!resourceGroupName || !providerNamespace || !resourceType || !resourceName) {
275
+ throw new Error("Operation 'deleteResource' requires resourceGroupName, providerNamespace, resourceType, and resourceName");
276
+ }
277
+ path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/${providerNamespace}/${resourceType}/${resourceName}`;
278
+ method = 'delete';
279
+ const deleteResourceKey = `${providerNamespace}/${resourceType}`;
280
+ apiVersion = apiVersion || defaultApiVersions[deleteResourceKey] || '2021-04-01';
281
+ break;
282
+ }
51
283
  }
52
- else if (lowerQuestion.includes("roles") || lowerQuestion.includes("directory roles")) {
53
- path = "/directoryRoles";
284
+ // Ensure API version is provided
285
+ if (!apiVersion && !queryParams['api-version']) {
286
+ throw new Error("Azure Resource Management API requires an 'apiVersion' parameter");
54
287
  }
55
- // If a GUID was found and not already used in the path, append it
56
- if (guidMatch && !path.includes(guidMatch[0])) {
57
- path = `${path}/${guidMatch[0]}`;
288
+ // Get Azure credential
289
+ const credential = getAzureCredential();
290
+ // Construct the base URL and path
291
+ const baseUrl = "https://management.azure.com";
292
+ let fullPath = path;
293
+ if (subscriptionId && !path.includes('/subscriptions/')) {
294
+ fullPath = `/subscriptions/${subscriptionId}${path.startsWith('/') ? path : `/${path}`}`;
58
295
  }
59
- // Build and execute the request
60
- let request = graphClient.api(path);
61
- // Execute the request
62
- const result = await request.get();
63
- // Format the response
64
- return {
65
- content: [
66
- {
67
- type: "text",
68
- text: `📊 Entra API Result (${path}):\n\n${JSON.stringify(result, null, 2)}`,
69
- },
70
- ],
296
+ // Add api-version and other query parameters
297
+ const params = new URLSearchParams(queryParams);
298
+ if (apiVersion)
299
+ params.set('api-version', apiVersion);
300
+ // Get access token
301
+ const tokenResponse = await credential.getToken("https://management.azure.com/.default");
302
+ if (!tokenResponse?.token)
303
+ throw new Error("Failed to acquire Azure access token");
304
+ // Prepare request options
305
+ const headers = {
306
+ 'Authorization': `Bearer ${tokenResponse.token}`,
307
+ 'Content-Type': 'application/json'
71
308
  };
309
+ const options = {
310
+ method: method.toUpperCase(),
311
+ headers
312
+ };
313
+ if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && body) {
314
+ options.body = JSON.stringify(body);
315
+ }
316
+ // Construct URL
317
+ const url = `${baseUrl}${fullPath}?${params.toString()}`;
318
+ // Execute request with pagination if needed
319
+ let result;
320
+ if (method === 'get' && fetchAllPages) {
321
+ // Fetch first page
322
+ const response = await fetch(url, options);
323
+ if (!response.ok) {
324
+ const errorText = await response.text();
325
+ throw new Error(`Azure API error: ${response.status} - ${errorText}`);
326
+ }
327
+ const firstPage = await response.json();
328
+ // If no pagination needed, return first page
329
+ if (!firstPage.nextLink) {
330
+ result = firstPage;
331
+ }
332
+ else {
333
+ // Collect all items from all pages
334
+ const allItems = [...(firstPage.value || [])];
335
+ let nextLink = firstPage.nextLink;
336
+ while (nextLink) {
337
+ const pageResponse = await fetch(nextLink, options);
338
+ if (!pageResponse.ok)
339
+ throw new Error(`Azure API pagination error: ${pageResponse.status}`);
340
+ const nextPage = await pageResponse.json();
341
+ if (nextPage.value)
342
+ allItems.push(...nextPage.value);
343
+ nextLink = nextPage.nextLink || null;
344
+ }
345
+ result = {
346
+ value: allItems,
347
+ count: allItems.length
348
+ };
349
+ }
350
+ }
351
+ else {
352
+ // Single page request
353
+ const response = await fetch(url, options);
354
+ if (!response.ok) {
355
+ const errorText = await response.text();
356
+ let errorDetail;
357
+ try {
358
+ errorDetail = JSON.parse(errorText);
359
+ }
360
+ catch {
361
+ errorDetail = errorText;
362
+ }
363
+ const error = new Error(`Azure API error: ${response.status}`);
364
+ error.status = response.status;
365
+ error.response = { body: errorDetail };
366
+ throw error;
367
+ }
368
+ const text = await response.text();
369
+ result = text ? JSON.parse(text) : { status: "Success" };
370
+ }
371
+ return formatApiResponse('Azure', method, path, result);
72
372
  }
73
373
  catch (err) {
74
- return {
75
- content: [
76
- {
77
- type: "text",
78
- text: `Error querying Entra API: ${err instanceof Error ? err.message : String(err)}`,
79
- },
80
- ],
81
- };
374
+ return formatErrorResponse(err, 'Azure');
82
375
  }
83
376
  });
84
- // Helper function to initialize the Microsoft Graph client
85
- function initGraphClient() {
86
- const tenantId = process.env.TENANT_ID;
87
- const clientId = process.env.CLIENT_ID;
88
- const clientSecret = process.env.CLIENT_SECRET;
89
- if (!tenantId || !clientId || !clientSecret) {
90
- throw new Error("Missing required Entra environment variables: TENANT_ID, CLIENT_ID, or CLIENT_SECRET");
91
- }
92
- const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
93
- const authProvider = new TokenCredentialAuthenticationProvider(credential, {
94
- scopes: ["https://graph.microsoft.com/.default"],
95
- });
96
- return Client.initWithMiddleware({
97
- authProvider: authProvider,
98
- });
99
- }
100
- // Start the server with stdio transport
377
+ // SERVER STARTUP
101
378
  async function main() {
102
- const transport = new StdioServerTransport();
103
- await server.connect(transport);
104
- console.error("EntraAware MCP Server running on stdio");
379
+ try {
380
+ const transport = new StdioServerTransport();
381
+ await server.connect(transport);
382
+ console.error("EntraAware MCP Server running on stdio");
383
+ }
384
+ catch (error) {
385
+ console.error("Error starting server:", error);
386
+ process.exit(1);
387
+ }
105
388
  }
106
389
  main().catch((error) => {
107
390
  console.error("Fatal error in main():", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north7/entraaware",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "build/index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "keywords": ["mcp-server", "entra", "azure-ad", "mcp", "microsoft-graph"],
17
17
  "author": "",
18
18
  "license": "ISC",
19
- "description": "MCP server for Microsoft Entra (Azure AD) integration",
19
+ "description": "MCP server for querying Entra and Azure",
20
20
  "files": [
21
21
  "build"
22
22
  ],