mcp-consultant-tools 7.0.0 → 8.0.0
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.
- package/README.md +15 -2
- package/build/LogAnalyticsService.js +409 -0
- package/build/index.js +716 -0
- package/build/utils/loganalytics-formatters.js +371 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MCP Consultant Tools
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server providing intelligent access to PowerPlatform/Dataverse, Azure DevOps, Figma, Azure Application Insights, and Azure SQL Database through an AI-friendly interface.
|
|
3
|
+
A Model Context Protocol (MCP) server providing intelligent access to PowerPlatform/Dataverse, Azure DevOps, Figma, Azure Application Insights, Azure Log Analytics, and Azure SQL Database through an AI-friendly interface.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -16,11 +16,12 @@ This MCP server enables AI assistants to:
|
|
|
16
16
|
- **Azure DevOps** (12 tools): Search wikis, manage work items, execute WIQL queries
|
|
17
17
|
- **Figma** (2 tools): Extract design data in simplified, AI-friendly format
|
|
18
18
|
- **Application Insights** (10 tools): Query telemetry, analyze exceptions, monitor performance, troubleshoot issues
|
|
19
|
+
- **Log Analytics** (10 tools): Query Azure Functions logs, analyze errors, monitor function performance, search workspace logs
|
|
19
20
|
- **Azure SQL Database** (9 tools): Explore database schema, query tables safely with read-only access, investigate database structure
|
|
20
21
|
|
|
21
22
|
All integrations are **optional** - configure only the services you need.
|
|
22
23
|
|
|
23
|
-
**Total:
|
|
24
|
+
**Total: 116 MCP tools & 23 prompts** providing comprehensive access to your development and operations lifecycle.
|
|
24
25
|
|
|
25
26
|
## Known limitations
|
|
26
27
|
- Cannot create Model-Driven-Apps
|
|
@@ -81,6 +82,12 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
81
82
|
"APPINSIGHTS_CLIENT_SECRET": "your-client-secret",
|
|
82
83
|
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]",
|
|
83
84
|
|
|
85
|
+
"LOGANALYTICS_AUTH_METHOD": "entra-id",
|
|
86
|
+
"LOGANALYTICS_TENANT_ID": "your-tenant-id",
|
|
87
|
+
"LOGANALYTICS_CLIENT_ID": "your-client-id",
|
|
88
|
+
"LOGANALYTICS_CLIENT_SECRET": "your-client-secret",
|
|
89
|
+
"LOGANALYTICS_RESOURCES": "[{\"id\":\"prod-functions\",\"name\":\"Production Functions\",\"workspaceId\":\"your-workspace-id\",\"active\":true}]",
|
|
90
|
+
|
|
84
91
|
"AZURE_SQL_SERVER": "yourserver.database.windows.net",
|
|
85
92
|
"AZURE_SQL_DATABASE": "yourdatabase",
|
|
86
93
|
"AZURE_SQL_USERNAME": "your-username",
|
|
@@ -130,6 +137,12 @@ Create `.vscode/mcp.json` in your project:
|
|
|
130
137
|
"APPINSIGHTS_CLIENT_SECRET": "your-client-secret",
|
|
131
138
|
"APPINSIGHTS_RESOURCES": "[{\"id\":\"prod-api\",\"name\":\"Production API\",\"appId\":\"your-app-id\",\"active\":true}]",
|
|
132
139
|
|
|
140
|
+
"LOGANALYTICS_AUTH_METHOD": "entra-id",
|
|
141
|
+
"LOGANALYTICS_TENANT_ID": "your-tenant-id",
|
|
142
|
+
"LOGANALYTICS_CLIENT_ID": "your-client-id",
|
|
143
|
+
"LOGANALYTICS_CLIENT_SECRET": "your-client-secret",
|
|
144
|
+
"LOGANALYTICS_RESOURCES": "[{\"id\":\"prod-functions\",\"name\":\"Production Functions\",\"workspaceId\":\"your-workspace-id\",\"active\":true}]",
|
|
145
|
+
|
|
133
146
|
"AZURE_SQL_SERVER": "yourserver.database.windows.net",
|
|
134
147
|
"AZURE_SQL_DATABASE": "yourdatabase",
|
|
135
148
|
"AZURE_SQL_USERNAME": "your-username",
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
export class LogAnalyticsService {
|
|
4
|
+
config;
|
|
5
|
+
msalClient = null;
|
|
6
|
+
accessToken = null;
|
|
7
|
+
tokenExpirationTime = 0;
|
|
8
|
+
baseUrl = 'https://api.loganalytics.io/v1';
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
// Initialize MSAL client if using Entra ID auth
|
|
12
|
+
if (this.config.authMethod === 'entra-id') {
|
|
13
|
+
if (!this.config.tenantId || !this.config.clientId || !this.config.clientSecret) {
|
|
14
|
+
throw new Error('Entra ID authentication requires tenantId, clientId, and clientSecret');
|
|
15
|
+
}
|
|
16
|
+
this.msalClient = new ConfidentialClientApplication({
|
|
17
|
+
auth: {
|
|
18
|
+
clientId: this.config.clientId,
|
|
19
|
+
clientSecret: this.config.clientSecret,
|
|
20
|
+
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get access token for Log Analytics API using Microsoft Entra ID OAuth
|
|
27
|
+
* Implements token caching with 5-minute buffer before expiry
|
|
28
|
+
*/
|
|
29
|
+
async getAccessToken() {
|
|
30
|
+
if (!this.msalClient) {
|
|
31
|
+
throw new Error('MSAL client not initialized. Use Entra ID authentication method.');
|
|
32
|
+
}
|
|
33
|
+
// Return cached token if still valid (with 5-minute buffer)
|
|
34
|
+
const currentTime = Date.now();
|
|
35
|
+
const bufferTime = 5 * 60 * 1000; // 5 minutes in milliseconds
|
|
36
|
+
if (this.accessToken && this.tokenExpirationTime > currentTime + bufferTime) {
|
|
37
|
+
return this.accessToken;
|
|
38
|
+
}
|
|
39
|
+
// Acquire new token
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.msalClient.acquireTokenByClientCredential({
|
|
42
|
+
scopes: ['https://api.loganalytics.io/.default'],
|
|
43
|
+
});
|
|
44
|
+
if (!result || !result.accessToken) {
|
|
45
|
+
throw new Error('Failed to acquire access token');
|
|
46
|
+
}
|
|
47
|
+
this.accessToken = result.accessToken;
|
|
48
|
+
this.tokenExpirationTime = result.expiresOn ? result.expiresOn.getTime() : 0;
|
|
49
|
+
return this.accessToken;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Failed to acquire access token: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get authorization headers based on authentication method
|
|
57
|
+
*/
|
|
58
|
+
async getAuthHeaders(resource) {
|
|
59
|
+
if (this.config.authMethod === 'entra-id') {
|
|
60
|
+
const token = await this.getAccessToken();
|
|
61
|
+
return {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else if (this.config.authMethod === 'api-key') {
|
|
67
|
+
if (!resource.apiKey) {
|
|
68
|
+
throw new Error(`API key not configured for resource: ${resource.id}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
'X-Api-Key': resource.apiKey,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw new Error(`Unsupported authentication method: ${this.config.authMethod}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Execute a KQL query against a Log Analytics workspace
|
|
81
|
+
*/
|
|
82
|
+
async executeQuery(resourceId, query, timespan) {
|
|
83
|
+
const resource = this.getResourceById(resourceId);
|
|
84
|
+
try {
|
|
85
|
+
const headers = await this.getAuthHeaders(resource);
|
|
86
|
+
const url = `${this.baseUrl}/workspaces/${resource.workspaceId}/query`;
|
|
87
|
+
const requestBody = { query };
|
|
88
|
+
if (timespan) {
|
|
89
|
+
requestBody.timespan = timespan;
|
|
90
|
+
}
|
|
91
|
+
const response = await axios.post(url, requestBody, {
|
|
92
|
+
headers,
|
|
93
|
+
timeout: 30000, // 30-second timeout
|
|
94
|
+
});
|
|
95
|
+
return response.data;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// Enhanced error handling
|
|
99
|
+
let errorMessage = 'Unknown error';
|
|
100
|
+
let errorDetails = {};
|
|
101
|
+
if (error.response) {
|
|
102
|
+
const status = error.response.status;
|
|
103
|
+
const data = error.response.data;
|
|
104
|
+
switch (status) {
|
|
105
|
+
case 401:
|
|
106
|
+
errorMessage = 'Authentication failed. Check your credentials and ensure the app registration has proper permissions.';
|
|
107
|
+
break;
|
|
108
|
+
case 403:
|
|
109
|
+
errorMessage = 'Access denied. Ensure the service principal has "Log Analytics Reader" role on the workspace.';
|
|
110
|
+
break;
|
|
111
|
+
case 429:
|
|
112
|
+
errorMessage = 'Rate limit exceeded. Reduce query frequency or upgrade authentication method.';
|
|
113
|
+
if (error.response.headers['retry-after']) {
|
|
114
|
+
errorMessage += ` Retry after ${error.response.headers['retry-after']} seconds.`;
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 400:
|
|
118
|
+
if (data && data.error) {
|
|
119
|
+
if (data.error.code === 'SyntaxError') {
|
|
120
|
+
errorMessage = `KQL syntax error: ${data.error.message}`;
|
|
121
|
+
}
|
|
122
|
+
else if (data.error.code === 'SemanticError') {
|
|
123
|
+
errorMessage = `KQL semantic error: ${data.error.message}. Check table/column names.`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
errorMessage = `Bad request: ${data.error.message}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
errorMessage = 'Bad request. Check your query syntax.';
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case 504:
|
|
134
|
+
errorMessage = 'Query timeout. Try reducing the time range or simplifying the query.';
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
errorMessage = `HTTP ${status}: ${data?.error?.message || error.message}`;
|
|
138
|
+
}
|
|
139
|
+
errorDetails = {
|
|
140
|
+
status,
|
|
141
|
+
code: data?.error?.code,
|
|
142
|
+
message: data?.error?.message,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
146
|
+
errorMessage = 'Network error: Unable to reach Log Analytics API. Check your internet connection.';
|
|
147
|
+
}
|
|
148
|
+
else if (error.code === 'ETIMEDOUT') {
|
|
149
|
+
errorMessage = 'Request timeout. The query took too long to execute.';
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
errorMessage = error.message;
|
|
153
|
+
}
|
|
154
|
+
throw new Error(errorMessage);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get workspace metadata (schema)
|
|
159
|
+
*/
|
|
160
|
+
async getMetadata(resourceId) {
|
|
161
|
+
const resource = this.getResourceById(resourceId);
|
|
162
|
+
try {
|
|
163
|
+
const headers = await this.getAuthHeaders(resource);
|
|
164
|
+
const url = `${this.baseUrl}/workspaces/${resource.workspaceId}/metadata`;
|
|
165
|
+
const response = await axios.get(url, {
|
|
166
|
+
headers,
|
|
167
|
+
timeout: 15000,
|
|
168
|
+
});
|
|
169
|
+
return response.data;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw new Error(`Failed to get metadata: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Test workspace access by executing a simple query
|
|
177
|
+
*/
|
|
178
|
+
async testWorkspaceAccess(resourceId) {
|
|
179
|
+
const resource = this.getResourceById(resourceId);
|
|
180
|
+
try {
|
|
181
|
+
// Execute a minimal query to test access
|
|
182
|
+
const result = await this.executeQuery(resourceId, 'print test="success"');
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
message: `Successfully connected to workspace: ${resource.name}`,
|
|
186
|
+
details: {
|
|
187
|
+
workspaceId: resource.workspaceId,
|
|
188
|
+
authMethod: this.config.authMethod,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
message: `Failed to access workspace: ${error.message}`,
|
|
196
|
+
details: {
|
|
197
|
+
workspaceId: resource.workspaceId,
|
|
198
|
+
error: error.message,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get recent events from any table
|
|
205
|
+
*/
|
|
206
|
+
async getRecentEvents(resourceId, tableName, timespan = 'PT1H', limit = 100) {
|
|
207
|
+
const query = `
|
|
208
|
+
${tableName}
|
|
209
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
210
|
+
| order by TimeGenerated desc
|
|
211
|
+
| take ${limit}
|
|
212
|
+
`.trim();
|
|
213
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Search logs across tables or specific table
|
|
217
|
+
*/
|
|
218
|
+
async searchLogs(resourceId, searchText, tableName, timespan = 'PT1H', limit = 100) {
|
|
219
|
+
const tableFilter = tableName || '*';
|
|
220
|
+
const query = `
|
|
221
|
+
${tableFilter}
|
|
222
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
223
|
+
| where * contains "${searchText}"
|
|
224
|
+
| order by TimeGenerated desc
|
|
225
|
+
| take ${limit}
|
|
226
|
+
`.trim();
|
|
227
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get Azure Function logs from FunctionAppLogs table
|
|
231
|
+
*/
|
|
232
|
+
async getFunctionLogs(resourceId, functionName, timespan = 'PT1H', severityLevel, limit = 100) {
|
|
233
|
+
let query = `
|
|
234
|
+
FunctionAppLogs
|
|
235
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
236
|
+
`;
|
|
237
|
+
if (functionName) {
|
|
238
|
+
query += `\n | where FunctionName == "${functionName}"`;
|
|
239
|
+
}
|
|
240
|
+
if (severityLevel !== undefined) {
|
|
241
|
+
query += `\n | where SeverityLevel >= ${severityLevel}`;
|
|
242
|
+
}
|
|
243
|
+
query += `
|
|
244
|
+
| order by TimeGenerated desc
|
|
245
|
+
| take ${limit}
|
|
246
|
+
| project TimeGenerated, FunctionName, Message, SeverityLevel, ExceptionDetails, HostInstanceId
|
|
247
|
+
`.trim();
|
|
248
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get Azure Function errors
|
|
252
|
+
*/
|
|
253
|
+
async getFunctionErrors(resourceId, functionName, timespan = 'PT1H', limit = 100) {
|
|
254
|
+
let query = `
|
|
255
|
+
FunctionAppLogs
|
|
256
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
257
|
+
| where ExceptionDetails != ""
|
|
258
|
+
`;
|
|
259
|
+
if (functionName) {
|
|
260
|
+
query += `\n | where FunctionName == "${functionName}"`;
|
|
261
|
+
}
|
|
262
|
+
query += `
|
|
263
|
+
| order by TimeGenerated desc
|
|
264
|
+
| take ${limit}
|
|
265
|
+
| project TimeGenerated, FunctionName, Message, ExceptionDetails, SeverityLevel, HostInstanceId
|
|
266
|
+
`.trim();
|
|
267
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get Azure Function execution statistics
|
|
271
|
+
*/
|
|
272
|
+
async getFunctionStats(resourceId, functionName, timespan = 'PT1H') {
|
|
273
|
+
let query = `
|
|
274
|
+
FunctionAppLogs
|
|
275
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
276
|
+
`;
|
|
277
|
+
if (functionName) {
|
|
278
|
+
query += `\n | where FunctionName == "${functionName}"`;
|
|
279
|
+
query += `
|
|
280
|
+
| summarize
|
|
281
|
+
TotalExecutions = count(),
|
|
282
|
+
ErrorCount = countif(ExceptionDetails != ""),
|
|
283
|
+
SuccessCount = countif(ExceptionDetails == ""),
|
|
284
|
+
UniqueHosts = dcount(HostInstanceId)
|
|
285
|
+
| extend SuccessRate = round(100.0 * SuccessCount / TotalExecutions, 2)
|
|
286
|
+
`.trim();
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
query += `
|
|
290
|
+
| summarize
|
|
291
|
+
TotalExecutions = count(),
|
|
292
|
+
ErrorCount = countif(ExceptionDetails != ""),
|
|
293
|
+
SuccessCount = countif(ExceptionDetails == ""),
|
|
294
|
+
UniqueFunctions = dcount(FunctionName),
|
|
295
|
+
UniqueHosts = dcount(HostInstanceId)
|
|
296
|
+
by FunctionName
|
|
297
|
+
| extend SuccessRate = round(100.0 * SuccessCount / TotalExecutions, 2)
|
|
298
|
+
| order by TotalExecutions desc
|
|
299
|
+
`.trim();
|
|
300
|
+
}
|
|
301
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get Azure Function invocations from traces or requests table
|
|
305
|
+
*/
|
|
306
|
+
async getFunctionInvocations(resourceId, functionName, timespan = 'PT1H', limit = 100) {
|
|
307
|
+
// Try requests table first (for HTTP-triggered functions)
|
|
308
|
+
let query = `
|
|
309
|
+
union isfuzzy=true requests, traces
|
|
310
|
+
| where TimeGenerated > ago(${this.convertTimespanToKQL(timespan)})
|
|
311
|
+
`;
|
|
312
|
+
if (functionName) {
|
|
313
|
+
query += `\n | where operation_Name contains "${functionName}" or name contains "${functionName}"`;
|
|
314
|
+
}
|
|
315
|
+
query += `
|
|
316
|
+
| order by TimeGenerated desc
|
|
317
|
+
| take ${limit}
|
|
318
|
+
| project TimeGenerated, operation_Name, name, success, resultCode, duration, timestamp
|
|
319
|
+
`.trim();
|
|
320
|
+
return this.executeQuery(resourceId, query, timespan);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get all configured resources
|
|
324
|
+
*/
|
|
325
|
+
getAllResources() {
|
|
326
|
+
return this.config.resources;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get only active resources
|
|
330
|
+
*/
|
|
331
|
+
getActiveResources() {
|
|
332
|
+
return this.config.resources.filter((r) => r.active);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get resource by ID and validate it's active
|
|
336
|
+
*/
|
|
337
|
+
getResourceById(resourceId) {
|
|
338
|
+
const resource = this.config.resources.find((r) => r.id === resourceId);
|
|
339
|
+
if (!resource) {
|
|
340
|
+
const availableIds = this.config.resources.map((r) => r.id).join(', ');
|
|
341
|
+
throw new Error(`Resource '${resourceId}' not found. Available resources: ${availableIds || 'none'}`);
|
|
342
|
+
}
|
|
343
|
+
if (!resource.active) {
|
|
344
|
+
throw new Error(`Resource '${resourceId}' is inactive. Set 'active: true' in configuration to enable it.`);
|
|
345
|
+
}
|
|
346
|
+
return resource;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Convert ISO 8601 duration to KQL timespan format
|
|
350
|
+
* PT1H -> 1h, P1D -> 1d, PT30M -> 30m, etc.
|
|
351
|
+
*/
|
|
352
|
+
convertTimespanToKQL(iso8601Duration) {
|
|
353
|
+
// Handle common patterns
|
|
354
|
+
const patterns = {
|
|
355
|
+
'PT15M': '15m',
|
|
356
|
+
'PT30M': '30m',
|
|
357
|
+
'PT1H': '1h',
|
|
358
|
+
'PT2H': '2h',
|
|
359
|
+
'PT6H': '6h',
|
|
360
|
+
'PT12H': '12h',
|
|
361
|
+
'PT24H': '24h',
|
|
362
|
+
'P1D': '1d',
|
|
363
|
+
'P2D': '2d',
|
|
364
|
+
'P7D': '7d',
|
|
365
|
+
'P30D': '30d',
|
|
366
|
+
};
|
|
367
|
+
if (patterns[iso8601Duration]) {
|
|
368
|
+
return patterns[iso8601Duration];
|
|
369
|
+
}
|
|
370
|
+
// Parse ISO 8601 duration
|
|
371
|
+
const regex = /P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
|
|
372
|
+
const match = iso8601Duration.match(regex);
|
|
373
|
+
if (!match) {
|
|
374
|
+
// If no match, return as-is (might be KQL format already)
|
|
375
|
+
return iso8601Duration;
|
|
376
|
+
}
|
|
377
|
+
const [, days, hours, minutes, seconds] = match;
|
|
378
|
+
// Convert to KQL format (use largest unit)
|
|
379
|
+
if (days)
|
|
380
|
+
return `${days}d`;
|
|
381
|
+
if (hours)
|
|
382
|
+
return `${hours}h`;
|
|
383
|
+
if (minutes)
|
|
384
|
+
return `${minutes}m`;
|
|
385
|
+
if (seconds)
|
|
386
|
+
return `${seconds}s`;
|
|
387
|
+
return '1h'; // Default fallback
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Validate KQL query (basic check)
|
|
391
|
+
*/
|
|
392
|
+
validateQuery(query) {
|
|
393
|
+
if (!query || query.trim().length === 0) {
|
|
394
|
+
return { valid: false, error: 'Query cannot be empty' };
|
|
395
|
+
}
|
|
396
|
+
// Check for dangerous operations (optional - KQL is read-only by nature)
|
|
397
|
+
const dangerousKeywords = ['invoke', 'execute', 'evaluate'];
|
|
398
|
+
const lowerQuery = query.toLowerCase();
|
|
399
|
+
for (const keyword of dangerousKeywords) {
|
|
400
|
+
if (lowerQuery.includes(keyword)) {
|
|
401
|
+
return {
|
|
402
|
+
valid: false,
|
|
403
|
+
error: `Query contains potentially dangerous keyword: ${keyword}`
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { valid: true };
|
|
408
|
+
}
|
|
409
|
+
}
|