@hailer/mcp 0.2.7 → 1.0.21
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/.claude/skills/client-bot-architecture/skill.md +340 -0
- package/.claude/skills/publish-hailer-app/SKILL.md +11 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +116 -84
- package/dist/bot/chat-bot.d.ts +31 -0
- package/dist/bot/chat-bot.js +356 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +71 -2
- package/dist/config.d.ts +15 -2
- package/dist/config.js +53 -3
- package/dist/lib/logger.js +11 -11
- package/dist/mcp/hailer-clients.js +12 -11
- package/dist/mcp/tool-registry.d.ts +4 -0
- package/dist/mcp/tool-registry.js +78 -1
- package/dist/mcp/tools/activity.js +47 -0
- package/dist/mcp/tools/discussion.js +44 -1
- package/dist/mcp/tools/metrics.d.ts +13 -0
- package/dist/mcp/tools/metrics.js +546 -0
- package/dist/mcp/tools/user.d.ts +1 -0
- package/dist/mcp/tools/user.js +94 -1
- package/dist/mcp/tools/workflow.js +109 -40
- package/dist/mcp/webhook-handler.js +7 -4
- package/dist/mcp-server.js +22 -6
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +6 -6
- package/scripts/test-hal-tools.ts +154 -0
- package/test-billing-server.js +136 -0
- package/dist/lib/discussion-lock.d.ts +0 -42
- package/dist/lib/discussion-lock.js +0 -110
- package/dist/mcp/tools/bot-config/constants.d.ts +0 -23
- package/dist/mcp/tools/bot-config/constants.js +0 -94
- package/dist/mcp/tools/bot-config/core.d.ts +0 -253
- package/dist/mcp/tools/bot-config/core.js +0 -2456
- package/dist/mcp/tools/bot-config/index.d.ts +0 -10
- package/dist/mcp/tools/bot-config/index.js +0 -59
- package/dist/mcp/tools/bot-config/tools.d.ts +0 -7
- package/dist/mcp/tools/bot-config/tools.js +0 -15
- package/dist/mcp/tools/bot-config/types.d.ts +0 -50
- package/dist/mcp/tools/bot-config/types.js +0 -6
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Victoria Metrics Tools - Query and List Metrics
|
|
4
|
+
*
|
|
5
|
+
* Tools for querying Hailer metrics from Victoria Metrics:
|
|
6
|
+
* - Query metrics with PromQL (READ)
|
|
7
|
+
* - List available metrics (READ)
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.searchUserForMetricsTool = exports.searchWorkspaceForMetricsTool = exports.listMetricsTool = exports.queryMetricTool = void 0;
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const tool_registry_1 = require("../tool-registry");
|
|
16
|
+
const index_1 = require("../utils/index");
|
|
17
|
+
const axios_1 = __importDefault(require("axios"));
|
|
18
|
+
const hailer_clients_1 = require("../hailer-clients");
|
|
19
|
+
const logger = (0, index_1.createLogger)({ component: 'metrics-tools' });
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// CONFIGURATION
|
|
22
|
+
// ============================================================================
|
|
23
|
+
const METRICS_CONFIG = {
|
|
24
|
+
host: process.env.METRICS_QUERY_HOST || 'observer.hailer.com',
|
|
25
|
+
port: Number(process.env.METRICS_QUERY_PORT) || 443,
|
|
26
|
+
secure: process.env.METRICS_QUERY_SECURE !== 'false',
|
|
27
|
+
user: process.env.METRICS_QUERY_USER || 'metrics',
|
|
28
|
+
password: process.env.METRICS_QUERY_PASSWORD || 'verysecuremetricspassword42',
|
|
29
|
+
};
|
|
30
|
+
// Metrics Admin API credentials (for workspace/user search - requires whitelisted user)
|
|
31
|
+
const METRICS_ADMIN_CONFIG = {
|
|
32
|
+
email: process.env.METRICS_ADMIN_EMAIL || '',
|
|
33
|
+
password: process.env.METRICS_ADMIN_PASSWORD || '',
|
|
34
|
+
apiBaseUrl: process.env.METRICS_ADMIN_API_URL || 'https://api.hailer.com',
|
|
35
|
+
};
|
|
36
|
+
logger.info('Metrics admin config loaded', {
|
|
37
|
+
email: METRICS_ADMIN_CONFIG.email || '(not set)',
|
|
38
|
+
apiBaseUrl: METRICS_ADMIN_CONFIG.apiBaseUrl,
|
|
39
|
+
hasPassword: !!METRICS_ADMIN_CONFIG.password,
|
|
40
|
+
});
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// HELPER FUNCTIONS
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Cached admin client for metrics operations
|
|
45
|
+
let metricsAdminClient = null;
|
|
46
|
+
let metricsAdminManager = null;
|
|
47
|
+
/**
|
|
48
|
+
* Get or create metrics admin client for privileged operations
|
|
49
|
+
*/
|
|
50
|
+
async function getMetricsAdminClient() {
|
|
51
|
+
if (!METRICS_ADMIN_CONFIG.email || !METRICS_ADMIN_CONFIG.password) {
|
|
52
|
+
throw new Error('METRICS_ADMIN_EMAIL and METRICS_ADMIN_PASSWORD must be set for workspace/user search');
|
|
53
|
+
}
|
|
54
|
+
// Return cached client if connected
|
|
55
|
+
if (metricsAdminClient && metricsAdminManager?.isConnected()) {
|
|
56
|
+
return metricsAdminClient;
|
|
57
|
+
}
|
|
58
|
+
// Clean up stale connection
|
|
59
|
+
if (metricsAdminManager) {
|
|
60
|
+
metricsAdminManager.disconnect();
|
|
61
|
+
}
|
|
62
|
+
logger.info('Creating metrics admin connection', {
|
|
63
|
+
email: METRICS_ADMIN_CONFIG.email,
|
|
64
|
+
apiBaseUrl: METRICS_ADMIN_CONFIG.apiBaseUrl,
|
|
65
|
+
});
|
|
66
|
+
// Create new connection using WebSocket (same as regular clients)
|
|
67
|
+
metricsAdminManager = new hailer_clients_1.HailerClientManager(METRICS_ADMIN_CONFIG.apiBaseUrl, METRICS_ADMIN_CONFIG.email, METRICS_ADMIN_CONFIG.password);
|
|
68
|
+
metricsAdminClient = await metricsAdminManager.connect();
|
|
69
|
+
logger.info('Metrics admin connected successfully');
|
|
70
|
+
return metricsAdminClient;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Make admin API request with special credentials via WebSocket
|
|
74
|
+
*/
|
|
75
|
+
async function metricsAdminRequest(method, params) {
|
|
76
|
+
const client = await getMetricsAdminClient();
|
|
77
|
+
return client.socket.request(method, params);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get Victoria Metrics base URL
|
|
81
|
+
*/
|
|
82
|
+
function getMetricsBaseUrl() {
|
|
83
|
+
const protocol = METRICS_CONFIG.secure ? 'https' : 'http';
|
|
84
|
+
const port = METRICS_CONFIG.port !== (METRICS_CONFIG.secure ? 443 : 80)
|
|
85
|
+
? `:${METRICS_CONFIG.port}`
|
|
86
|
+
: '';
|
|
87
|
+
return `${protocol}://${METRICS_CONFIG.host}${port}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get Basic Auth credentials
|
|
91
|
+
*/
|
|
92
|
+
function getAuthHeader() {
|
|
93
|
+
return {
|
|
94
|
+
username: METRICS_CONFIG.user,
|
|
95
|
+
password: METRICS_CONFIG.password,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse time range string to PromQL duration
|
|
100
|
+
*/
|
|
101
|
+
function parseTimeRange(timeRange) {
|
|
102
|
+
const validRanges = ['1h', '24h', '3d', '7d', '30d'];
|
|
103
|
+
if (!validRanges.includes(timeRange)) {
|
|
104
|
+
throw new Error(`Invalid time range "${timeRange}". Valid options: ${validRanges.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
return timeRange;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build PromQL query from parameters
|
|
110
|
+
*/
|
|
111
|
+
function buildPromQLQuery(metric, timeRange, aggregation, groupBy, filters) {
|
|
112
|
+
// Build label filters
|
|
113
|
+
let labelFilters = '';
|
|
114
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
115
|
+
const filterPairs = Object.entries(filters).map(([key, value]) => `${key}="${value}"`);
|
|
116
|
+
labelFilters = filterPairs.join(',');
|
|
117
|
+
}
|
|
118
|
+
// Build metric selector
|
|
119
|
+
const metricSelector = labelFilters
|
|
120
|
+
? `${metric}{${labelFilters}}`
|
|
121
|
+
: metric;
|
|
122
|
+
// Build aggregation with groupBy
|
|
123
|
+
const groupByClause = groupBy && groupBy.length > 0
|
|
124
|
+
? ` by (${groupBy.join(', ')})`
|
|
125
|
+
: '';
|
|
126
|
+
// Build final query
|
|
127
|
+
const query = `${aggregation}${groupByClause} (increase(${metricSelector}[${timeRange}]))`;
|
|
128
|
+
return query;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format query results for display
|
|
132
|
+
*/
|
|
133
|
+
function formatQueryResults(data) {
|
|
134
|
+
if (!data || !data.result || data.result.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
formatted: 'No data returned for this query.',
|
|
137
|
+
raw: data,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Single value result
|
|
141
|
+
if (data.result.length === 1 && !data.result[0].metric) {
|
|
142
|
+
const value = data.result[0].value[1];
|
|
143
|
+
return {
|
|
144
|
+
formatted: `Result: ${value}`,
|
|
145
|
+
raw: { value: Number(value) },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Multiple values or grouped results
|
|
149
|
+
const results = data.result.map((item) => {
|
|
150
|
+
const labels = item.metric || {};
|
|
151
|
+
const value = item.value[1];
|
|
152
|
+
return {
|
|
153
|
+
labels,
|
|
154
|
+
value: Number(value),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
// Format as table
|
|
158
|
+
let formatted = `Found ${results.length} result(s):\n\n`;
|
|
159
|
+
for (const result of results) {
|
|
160
|
+
const labelStr = Object.entries(result.labels)
|
|
161
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
162
|
+
.join(', ');
|
|
163
|
+
formatted += `- ${labelStr ? `{${labelStr}}` : 'total'}: ${result.value}\n`;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
formatted,
|
|
167
|
+
raw: results,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// TOOL 1: QUERY METRIC
|
|
172
|
+
// ============================================================================
|
|
173
|
+
/**
|
|
174
|
+
* Generate ASCII bar chart from data
|
|
175
|
+
*/
|
|
176
|
+
function generateAsciiChart(data, options = {}) {
|
|
177
|
+
const { maxWidth = 40, showPercent = true } = options;
|
|
178
|
+
if (data.length === 0)
|
|
179
|
+
return 'No data to display';
|
|
180
|
+
const maxValue = Math.max(...data.map(d => d.value));
|
|
181
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
182
|
+
// Find max label length for alignment
|
|
183
|
+
const maxLabelLen = Math.max(...data.map(d => d.label.length), 10);
|
|
184
|
+
const lines = data.map(item => {
|
|
185
|
+
const barLength = maxValue > 0 ? Math.round((item.value / maxValue) * maxWidth) : 0;
|
|
186
|
+
const bar = '█'.repeat(barLength);
|
|
187
|
+
const label = item.label.padEnd(maxLabelLen);
|
|
188
|
+
const valueStr = item.value.toLocaleString();
|
|
189
|
+
const percent = showPercent && total > 0
|
|
190
|
+
? ` (${Math.round(item.value / total * 100)}%)`
|
|
191
|
+
: '';
|
|
192
|
+
return `${label} ${bar} ${valueStr}${percent}`;
|
|
193
|
+
});
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Format grouped results as ASCII chart
|
|
198
|
+
*/
|
|
199
|
+
function formatAsChart(data, groupByLabel) {
|
|
200
|
+
if (!data || !data.result || data.result.length === 0) {
|
|
201
|
+
return 'No data to display';
|
|
202
|
+
}
|
|
203
|
+
const chartData = data.result
|
|
204
|
+
.map((item) => ({
|
|
205
|
+
label: item.metric?.[groupByLabel] || 'unknown',
|
|
206
|
+
value: Math.round(parseFloat(item.value?.[1] || 0)),
|
|
207
|
+
}))
|
|
208
|
+
.filter((item) => item.value > 0)
|
|
209
|
+
.sort((a, b) => b.value - a.value)
|
|
210
|
+
.slice(0, 10); // Top 10
|
|
211
|
+
const total = chartData.reduce((sum, d) => sum + d.value, 0);
|
|
212
|
+
let output = generateAsciiChart(chartData);
|
|
213
|
+
output += `\n\nTotal: ${total.toLocaleString()}`;
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// TOOL 1: QUERY METRIC
|
|
218
|
+
// ============================================================================
|
|
219
|
+
exports.queryMetricTool = {
|
|
220
|
+
name: 'query_metric',
|
|
221
|
+
group: tool_registry_1.ToolGroup.READ,
|
|
222
|
+
description: `Query metrics from Victoria Metrics using PromQL. Available metrics:
|
|
223
|
+
- hailer.activity.create - Activities created (labels: workspace, process, team, userRole)
|
|
224
|
+
- hailer.message.create - Messages sent (labels: workspace, discussion, userRole)
|
|
225
|
+
- hailer.file.upload - Files uploaded (labels: workspace)
|
|
226
|
+
- hailer.telemetry - UI events (labels: workspace, user, element)
|
|
227
|
+
|
|
228
|
+
Supports ASCII chart visualization for grouped data (format: "chart")`,
|
|
229
|
+
schema: zod_1.z.object({
|
|
230
|
+
metric: zod_1.z.string().describe('Metric name (e.g., "hailer.activity.create")'),
|
|
231
|
+
timeRange: zod_1.z.enum(['1h', '24h', '3d', '7d', '30d']).optional().default('24h').describe('Time range: 1h, 24h, 3d, 7d, 30d'),
|
|
232
|
+
aggregation: zod_1.z.enum(['sum', 'avg', 'max', 'min', 'count']).optional().default('sum').describe('Aggregation function'),
|
|
233
|
+
groupBy: zod_1.z.preprocess((val) => {
|
|
234
|
+
if (typeof val === 'string') {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(val);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return val.split(',').map(s => s.trim()).filter(Boolean);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return val;
|
|
243
|
+
}, zod_1.z.array(zod_1.z.string()).optional()).describe('Group by labels like ["workspace", "process"] (optional)'),
|
|
244
|
+
filters: zod_1.z.preprocess((val) => typeof val === 'string' ? JSON.parse(val) : val, zod_1.z.record(zod_1.z.string()).optional()).describe('Label filters like { "workspace": "abc123", "userRole": "admin" } (optional)'),
|
|
245
|
+
format: zod_1.z.enum(["json", "chart"]).optional().default("json").describe("Output format: 'json' for raw data, 'chart' for ASCII bar chart"),
|
|
246
|
+
}),
|
|
247
|
+
async execute(args, _context) {
|
|
248
|
+
try {
|
|
249
|
+
logger.info('Querying metric', {
|
|
250
|
+
metric: args.metric,
|
|
251
|
+
timeRange: args.timeRange,
|
|
252
|
+
aggregation: args.aggregation,
|
|
253
|
+
groupBy: args.groupBy,
|
|
254
|
+
filters: args.filters,
|
|
255
|
+
});
|
|
256
|
+
// Parse and validate time range
|
|
257
|
+
const timeRange = parseTimeRange(args.timeRange || '24h');
|
|
258
|
+
// Build PromQL query
|
|
259
|
+
const promqlQuery = buildPromQLQuery(args.metric, timeRange, args.aggregation || 'sum', args.groupBy, args.filters);
|
|
260
|
+
logger.debug('Built PromQL query', { query: promqlQuery });
|
|
261
|
+
// Execute query
|
|
262
|
+
const baseUrl = getMetricsBaseUrl();
|
|
263
|
+
const url = `${baseUrl}/api/v1/query`;
|
|
264
|
+
const response = await axios_1.default.get(url, {
|
|
265
|
+
params: { query: promqlQuery },
|
|
266
|
+
auth: getAuthHeader(),
|
|
267
|
+
timeout: 10000,
|
|
268
|
+
});
|
|
269
|
+
if (response.data.status !== 'success') {
|
|
270
|
+
throw new Error(`Query failed: ${response.data.error || 'Unknown error'}`);
|
|
271
|
+
}
|
|
272
|
+
// Format results
|
|
273
|
+
const { formatted, raw } = formatQueryResults(response.data.data);
|
|
274
|
+
// Format output based on requested format
|
|
275
|
+
const format = args.format || 'json';
|
|
276
|
+
let responseText;
|
|
277
|
+
if (format === 'chart' && args.groupBy && args.groupBy.length > 0) {
|
|
278
|
+
// ASCII chart for grouped data
|
|
279
|
+
const chartTitle = `📊 ${args.metric} by ${args.groupBy[0]} (${args.timeRange})\n${'─'.repeat(50)}\n`;
|
|
280
|
+
responseText = chartTitle + formatAsChart(response.data.data, args.groupBy[0]);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// JSON format (default)
|
|
284
|
+
responseText = `📊 **Metric Query Results**
|
|
285
|
+
|
|
286
|
+
**Query:** \`${promqlQuery}\`
|
|
287
|
+
**Time Range:** ${args.timeRange}
|
|
288
|
+
**Aggregation:** ${args.aggregation}
|
|
289
|
+
|
|
290
|
+
${formatted}
|
|
291
|
+
|
|
292
|
+
**Raw Data:**
|
|
293
|
+
\`\`\`json
|
|
294
|
+
${JSON.stringify(raw, null, 2)}
|
|
295
|
+
\`\`\``;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: responseText,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger.error('Failed to query metric', error, {
|
|
308
|
+
metric: args.metric,
|
|
309
|
+
timeRange: args.timeRange,
|
|
310
|
+
});
|
|
311
|
+
const errorMessage = error instanceof Error
|
|
312
|
+
? error.message
|
|
313
|
+
: String(error);
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: `❌ Failed to query metric: ${errorMessage}\n\n💡 **Troubleshooting:**\n- Verify the metric name is correct (use list_metrics to see available metrics)\n- Check time range is valid (1h, 24h, 3d, 7d, 30d)\n- Ensure filters match existing labels\n- Verify Victoria Metrics connection settings`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// TOOL 2: LIST METRICS
|
|
327
|
+
// ============================================================================
|
|
328
|
+
exports.listMetricsTool = {
|
|
329
|
+
name: 'list_metrics',
|
|
330
|
+
group: tool_registry_1.ToolGroup.READ,
|
|
331
|
+
description: 'List all available Hailer metrics from Victoria Metrics',
|
|
332
|
+
schema: zod_1.z.object({}),
|
|
333
|
+
async execute(_args, _context) {
|
|
334
|
+
try {
|
|
335
|
+
logger.info('Listing available metrics');
|
|
336
|
+
// Query Victoria Metrics for all metric names
|
|
337
|
+
const baseUrl = getMetricsBaseUrl();
|
|
338
|
+
const url = `${baseUrl}/api/v1/label/__name__/values`;
|
|
339
|
+
const response = await axios_1.default.get(url, {
|
|
340
|
+
auth: getAuthHeader(),
|
|
341
|
+
timeout: 10000,
|
|
342
|
+
});
|
|
343
|
+
if (response.data.status !== 'success') {
|
|
344
|
+
throw new Error(`Query failed: ${response.data.error || 'Unknown error'}`);
|
|
345
|
+
}
|
|
346
|
+
const metrics = response.data.data || [];
|
|
347
|
+
// Filter for Hailer metrics only
|
|
348
|
+
const hailerMetrics = metrics.filter((m) => m.startsWith('hailer.'));
|
|
349
|
+
// Group metrics by category
|
|
350
|
+
const categorized = {
|
|
351
|
+
activity: [],
|
|
352
|
+
message: [],
|
|
353
|
+
file: [],
|
|
354
|
+
telemetry: [],
|
|
355
|
+
other: [],
|
|
356
|
+
};
|
|
357
|
+
for (const metric of hailerMetrics) {
|
|
358
|
+
if (metric.includes('activity')) {
|
|
359
|
+
categorized.activity.push(metric);
|
|
360
|
+
}
|
|
361
|
+
else if (metric.includes('message')) {
|
|
362
|
+
categorized.message.push(metric);
|
|
363
|
+
}
|
|
364
|
+
else if (metric.includes('file')) {
|
|
365
|
+
categorized.file.push(metric);
|
|
366
|
+
}
|
|
367
|
+
else if (metric.includes('telemetry')) {
|
|
368
|
+
categorized.telemetry.push(metric);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
categorized.other.push(metric);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Format output
|
|
375
|
+
let formatted = `📊 **Available Hailer Metrics** (${hailerMetrics.length} total)\n\n`;
|
|
376
|
+
if (categorized.activity.length > 0) {
|
|
377
|
+
formatted += `**Activity Metrics:**\n`;
|
|
378
|
+
for (const metric of categorized.activity) {
|
|
379
|
+
formatted += `- \`${metric}\` - Activities created/updated\n`;
|
|
380
|
+
}
|
|
381
|
+
formatted += '\n';
|
|
382
|
+
}
|
|
383
|
+
if (categorized.message.length > 0) {
|
|
384
|
+
formatted += `**Message Metrics:**\n`;
|
|
385
|
+
for (const metric of categorized.message) {
|
|
386
|
+
formatted += `- \`${metric}\` - Messages and discussions\n`;
|
|
387
|
+
}
|
|
388
|
+
formatted += '\n';
|
|
389
|
+
}
|
|
390
|
+
if (categorized.file.length > 0) {
|
|
391
|
+
formatted += `**File Metrics:**\n`;
|
|
392
|
+
for (const metric of categorized.file) {
|
|
393
|
+
formatted += `- \`${metric}\` - File uploads and operations\n`;
|
|
394
|
+
}
|
|
395
|
+
formatted += '\n';
|
|
396
|
+
}
|
|
397
|
+
if (categorized.telemetry.length > 0) {
|
|
398
|
+
formatted += `**Telemetry Metrics:**\n`;
|
|
399
|
+
for (const metric of categorized.telemetry) {
|
|
400
|
+
formatted += `- \`${metric}\` - UI events and user interactions\n`;
|
|
401
|
+
}
|
|
402
|
+
formatted += '\n';
|
|
403
|
+
}
|
|
404
|
+
if (categorized.other.length > 0) {
|
|
405
|
+
formatted += `**Other Metrics:**\n`;
|
|
406
|
+
for (const metric of categorized.other) {
|
|
407
|
+
formatted += `- \`${metric}\`\n`;
|
|
408
|
+
}
|
|
409
|
+
formatted += '\n';
|
|
410
|
+
}
|
|
411
|
+
formatted += `\n💡 **Usage:**\nUse \`query_metric\` with any of these metric names to query data.\n\n`;
|
|
412
|
+
formatted += `**Common Labels:**\n- workspace: Workspace ID\n- process: Workflow ID\n- team: Team ID\n- userRole: User role (admin, member, etc.)\n- discussion: Discussion ID\n- user: User ID\n- element: UI element name`;
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: 'text',
|
|
417
|
+
text: formatted,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
logger.error('Failed to list metrics', error);
|
|
424
|
+
const errorMessage = error instanceof Error
|
|
425
|
+
? error.message
|
|
426
|
+
: String(error);
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: `❌ Failed to list metrics: ${errorMessage}\n\n💡 **Troubleshooting:**\n- Verify Victoria Metrics connection settings\n- Check network connectivity to ${METRICS_CONFIG.host}\n- Ensure credentials are correct`,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// TOOL 3: SEARCH WORKSPACE FOR METRICS
|
|
440
|
+
// ============================================================================
|
|
441
|
+
exports.searchWorkspaceForMetricsTool = {
|
|
442
|
+
name: 'search_workspace_for_metrics',
|
|
443
|
+
group: tool_registry_1.ToolGroup.READ,
|
|
444
|
+
description: `Search for workspaces by name to get their IDs for metric queries.
|
|
445
|
+
Use this tool when user mentions a workspace by name (e.g. "Sales Team") to find its ID for filtering metrics.`,
|
|
446
|
+
schema: zod_1.z.object({
|
|
447
|
+
name: zod_1.z.string().min(3).describe("Workspace name to search (min 3 characters)"),
|
|
448
|
+
limit: zod_1.z.number().min(1).max(100).optional().default(20).describe("Maximum results to return (default 20, max 100)"),
|
|
449
|
+
}),
|
|
450
|
+
async execute(args, _context) {
|
|
451
|
+
try {
|
|
452
|
+
logger.info('Searching workspace for metrics', { name: args.name });
|
|
453
|
+
// Use admin credentials for this privileged operation
|
|
454
|
+
const result = await metricsAdminRequest('v3.metrics.workspaceSearch', [{ name: args.name }]);
|
|
455
|
+
if (!result || result.length === 0) {
|
|
456
|
+
return {
|
|
457
|
+
content: [{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: `No workspaces found matching "${args.name}"`,
|
|
460
|
+
}],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const totalCount = result.length;
|
|
464
|
+
const limitedResults = result.slice(0, args.limit || 20);
|
|
465
|
+
const workspaceList = limitedResults
|
|
466
|
+
.map((ws) => `- ${ws.name} (ID: ${ws._id})`)
|
|
467
|
+
.join('\n');
|
|
468
|
+
const truncationNote = totalCount > limitedResults.length
|
|
469
|
+
? `\n\n⚠️ Showing ${limitedResults.length} of ${totalCount} results. Use a more specific search term or increase limit.`
|
|
470
|
+
: '';
|
|
471
|
+
return {
|
|
472
|
+
content: [{
|
|
473
|
+
type: 'text',
|
|
474
|
+
text: `Found ${totalCount} workspace(s)${totalCount > limitedResults.length ? ` (showing first ${limitedResults.length})` : ''}:\n${workspaceList}${truncationNote}\n\n💡 Use the ID in metric queries with filters: { "workspace": "ID" }`,
|
|
475
|
+
}],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
logger.error('Failed to search workspaces for metrics', error);
|
|
480
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
481
|
+
return {
|
|
482
|
+
content: [{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text: `❌ Error searching workspaces: ${errorMessage}`,
|
|
485
|
+
}],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
// ============================================================================
|
|
491
|
+
// TOOL 4: SEARCH USER FOR METRICS
|
|
492
|
+
// ============================================================================
|
|
493
|
+
exports.searchUserForMetricsTool = {
|
|
494
|
+
name: 'search_user_for_metrics',
|
|
495
|
+
group: tool_registry_1.ToolGroup.READ,
|
|
496
|
+
description: `Look up user details by ID for metric analysis.
|
|
497
|
+
Use this after getting user IDs from grouped metric results to see who they are.`,
|
|
498
|
+
schema: zod_1.z.object({
|
|
499
|
+
userIds: zod_1.z.preprocess((val) => {
|
|
500
|
+
if (typeof val === 'string') {
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(val);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return val.split(',').map(s => s.trim()).filter(Boolean);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return val;
|
|
509
|
+
}, zod_1.z.array(zod_1.z.string())).describe("Array of user IDs to look up"),
|
|
510
|
+
}),
|
|
511
|
+
async execute(args, _context) {
|
|
512
|
+
try {
|
|
513
|
+
logger.info('Looking up users for metrics', { userIds: args.userIds });
|
|
514
|
+
// Use admin credentials for this privileged operation
|
|
515
|
+
const result = await metricsAdminRequest('v3.metrics.user.list', [{ userIds: args.userIds }]);
|
|
516
|
+
if (!result || result.length === 0) {
|
|
517
|
+
return {
|
|
518
|
+
content: [{
|
|
519
|
+
type: 'text',
|
|
520
|
+
text: `No users found for provided IDs`,
|
|
521
|
+
}],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const userList = result
|
|
525
|
+
.map((user) => `- ${user.name || 'No name'} (${user.email || 'no email'}) - ID: ${user._id}`)
|
|
526
|
+
.join('\n');
|
|
527
|
+
return {
|
|
528
|
+
content: [{
|
|
529
|
+
type: 'text',
|
|
530
|
+
text: `Found ${result.length} user(s):\n${userList}`,
|
|
531
|
+
}],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
logger.error('Failed to look up users for metrics', error);
|
|
536
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
537
|
+
return {
|
|
538
|
+
content: [{
|
|
539
|
+
type: 'text',
|
|
540
|
+
text: `❌ Error looking up users: ${errorMessage}`,
|
|
541
|
+
}],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
//# sourceMappingURL=metrics.js.map
|
package/dist/mcp/tools/user.d.ts
CHANGED
package/dist/mcp/tools/user.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Get initialization data (READ) - currently disabled
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.searchWorkspaceUsersTool = void 0;
|
|
10
|
+
exports.getWorkspaceBalanceTool = exports.searchWorkspaceUsersTool = void 0;
|
|
11
11
|
const zod_1 = require("zod");
|
|
12
12
|
const tool_registry_1 = require("../tool-registry");
|
|
13
13
|
const index_1 = require("../utils/index");
|
|
@@ -80,4 +80,97 @@ exports.searchWorkspaceUsersTool = {
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// TOOL 2: GET WORKSPACE BALANCE
|
|
85
|
+
// ============================================================================
|
|
86
|
+
const getWorkspaceBalanceDescription = `Check the current token balance for the workspace. Use this when users ask about their balance, credits, or usage limits.`;
|
|
87
|
+
exports.getWorkspaceBalanceTool = {
|
|
88
|
+
name: 'get_workspace_balance',
|
|
89
|
+
group: tool_registry_1.ToolGroup.READ,
|
|
90
|
+
description: getWorkspaceBalanceDescription,
|
|
91
|
+
schema: zod_1.z.object({
|
|
92
|
+
workspaceId: zod_1.z
|
|
93
|
+
.string()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe("The workspace ID to check balance for. If not provided, uses the current workspace context."),
|
|
96
|
+
}),
|
|
97
|
+
async execute(args, context) {
|
|
98
|
+
logger.debug('Checking workspace balance', {
|
|
99
|
+
workspaceId: args.workspaceId,
|
|
100
|
+
apiKey: context.apiKey.substring(0, 8) + '...'
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
// Get workspace ID from args or default to current workspace
|
|
104
|
+
const workspaceId = (0, tool_helpers_1.getResolvedWorkspaceId)(args, context);
|
|
105
|
+
if (!workspaceId) {
|
|
106
|
+
return (0, tool_helpers_1.missingWorkspaceCacheResponse)();
|
|
107
|
+
}
|
|
108
|
+
// Call the Hailer API to get token balance
|
|
109
|
+
const result = await context.hailer.request('v3.mcp.getTokenBalance', [workspaceId]);
|
|
110
|
+
// Parse the balance from response
|
|
111
|
+
const balance = typeof result?.balance === "number" ? result.balance :
|
|
112
|
+
typeof result === "number" ? result : 0;
|
|
113
|
+
// Determine status
|
|
114
|
+
let status;
|
|
115
|
+
let hasBalance;
|
|
116
|
+
if (balance < 0) {
|
|
117
|
+
status = 'NEGATIVE';
|
|
118
|
+
hasBalance = false;
|
|
119
|
+
}
|
|
120
|
+
else if (balance === 0) {
|
|
121
|
+
status = 'EMPTY';
|
|
122
|
+
hasBalance = false;
|
|
123
|
+
}
|
|
124
|
+
else if (balance < 100) {
|
|
125
|
+
status = 'LOW';
|
|
126
|
+
hasBalance = true;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
status = 'OK';
|
|
130
|
+
hasBalance = true;
|
|
131
|
+
}
|
|
132
|
+
// Format the balance with commas for readability
|
|
133
|
+
const formattedBalance = balance.toLocaleString();
|
|
134
|
+
// Build response message
|
|
135
|
+
let message;
|
|
136
|
+
if (status === 'OK') {
|
|
137
|
+
message = `Workspace has ${formattedBalance} tokens available`;
|
|
138
|
+
}
|
|
139
|
+
else if (status === 'LOW') {
|
|
140
|
+
message = `Workspace balance is low: ${formattedBalance} tokens remaining`;
|
|
141
|
+
}
|
|
142
|
+
else if (status === 'EMPTY') {
|
|
143
|
+
message = `Workspace has no tokens available`;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
message = `Workspace balance is negative: ${formattedBalance} tokens`;
|
|
147
|
+
}
|
|
148
|
+
const response = {
|
|
149
|
+
balance,
|
|
150
|
+
status,
|
|
151
|
+
hasBalance,
|
|
152
|
+
message,
|
|
153
|
+
workspaceId,
|
|
154
|
+
};
|
|
155
|
+
logger.debug('Workspace balance retrieved', {
|
|
156
|
+
workspaceId,
|
|
157
|
+
balance,
|
|
158
|
+
status
|
|
159
|
+
});
|
|
160
|
+
// Return formatted response
|
|
161
|
+
const statusEmoji = status === 'OK' ? '✅' :
|
|
162
|
+
status === 'LOW' ? '⚠️' :
|
|
163
|
+
'❌';
|
|
164
|
+
return (0, index_1.textResponse)(`${statusEmoji} **Workspace Balance**\n\n` +
|
|
165
|
+
`- Balance: **${formattedBalance}** tokens\n` +
|
|
166
|
+
`- Status: ${status}\n` +
|
|
167
|
+
`- Has sufficient balance: ${hasBalance ? 'Yes' : 'No'}\n\n` +
|
|
168
|
+
`${message}`);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
logger.error("Error checking workspace balance", error);
|
|
172
|
+
return (0, index_1.errorResponse)(`Error checking workspace balance: ${(0, index_1.getErrorMessage)(error)}`);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
83
176
|
//# sourceMappingURL=user.js.map
|