@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
|
@@ -91,23 +91,24 @@ class HailerClientManager {
|
|
|
91
91
|
password: this.password,
|
|
92
92
|
...(isLocalDev && { rejectUnauthorized: false }), // Add for local dev with self-signed certs
|
|
93
93
|
};
|
|
94
|
-
// Track timeout
|
|
94
|
+
// Track timeout with single settled flag to prevent race conditions
|
|
95
95
|
let timeoutId = null;
|
|
96
|
-
let
|
|
96
|
+
let settled = false;
|
|
97
97
|
try {
|
|
98
98
|
// Create socket client using @hailer/cli with cancellable timeout
|
|
99
99
|
this.socketClient = (await Promise.race([
|
|
100
100
|
cli_1.Client.create(clientOptions).then(client => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (!settled) {
|
|
102
|
+
settled = true;
|
|
103
|
+
if (timeoutId)
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
105
|
}
|
|
106
106
|
return client;
|
|
107
107
|
}),
|
|
108
108
|
new Promise((_, reject) => {
|
|
109
109
|
timeoutId = setTimeout(() => {
|
|
110
|
-
if (!
|
|
110
|
+
if (!settled) {
|
|
111
|
+
settled = true;
|
|
111
112
|
reject(new Error(`Timeout connecting to: ${this.host}`));
|
|
112
113
|
}
|
|
113
114
|
}, 30000);
|
|
@@ -115,10 +116,10 @@ class HailerClientManager {
|
|
|
115
116
|
]));
|
|
116
117
|
}
|
|
117
118
|
catch (error) {
|
|
118
|
-
// Ensure timeout is cleared on error
|
|
119
|
-
if (timeoutId && !
|
|
119
|
+
// Ensure timeout is cleared on error
|
|
120
|
+
if (timeoutId && !settled) {
|
|
121
|
+
settled = true;
|
|
120
122
|
clearTimeout(timeoutId);
|
|
121
|
-
timeoutCleared = true;
|
|
122
123
|
}
|
|
123
124
|
logger.error('Failed to create socket client', error, { username: this.username });
|
|
124
125
|
throw error;
|
|
@@ -345,7 +346,7 @@ function registerBotCredentials(botId, email, password, options) {
|
|
|
345
346
|
config_1.environment.CLIENT_CONFIGS[apiKey] = {
|
|
346
347
|
email,
|
|
347
348
|
password,
|
|
348
|
-
apiBaseUrl: 'https://api.hailer.com',
|
|
349
|
+
apiBaseUrl: config_1.environment.BOT_API_BASE_URL || 'https://api.hailer.com',
|
|
349
350
|
...(options?.allowedGroups && { allowedGroups: options.allowedGroups }),
|
|
350
351
|
};
|
|
351
352
|
logger.info('Bot credentials registered', {
|
|
@@ -109,6 +109,10 @@ export declare class ToolRegistry {
|
|
|
109
109
|
* Execute tool with validation
|
|
110
110
|
*/
|
|
111
111
|
executeTool(name: string, args: any, context: UserContext): Promise<any>;
|
|
112
|
+
/**
|
|
113
|
+
* Pre-transform install_workflow args to fix common LLM mistakes BEFORE validation
|
|
114
|
+
*/
|
|
115
|
+
private preTransformInstallWorkflow;
|
|
112
116
|
/**
|
|
113
117
|
* Get tool metadata (for access control checks)
|
|
114
118
|
*/
|
|
@@ -277,9 +277,14 @@ class ToolRegistry {
|
|
|
277
277
|
name,
|
|
278
278
|
apiKey: context.apiKey.substring(0, 8) + '...'
|
|
279
279
|
});
|
|
280
|
+
// Pre-transform for specific tools (fix common LLM mistakes before validation)
|
|
281
|
+
let transformedArgs = args;
|
|
282
|
+
if (name === 'install_workflow' && args.workflowTemplates) {
|
|
283
|
+
transformedArgs = this.preTransformInstallWorkflow(args);
|
|
284
|
+
}
|
|
280
285
|
// Zod validation
|
|
281
286
|
try {
|
|
282
|
-
const validated = tool.schema.parse(
|
|
287
|
+
const validated = tool.schema.parse(transformedArgs);
|
|
283
288
|
return await tool.execute(validated, context);
|
|
284
289
|
}
|
|
285
290
|
catch (error) {
|
|
@@ -318,6 +323,78 @@ Then retry \`${name}\` with the correct format from the skill examples.` : `š”
|
|
|
318
323
|
throw error;
|
|
319
324
|
}
|
|
320
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Pre-transform install_workflow args to fix common LLM mistakes BEFORE validation
|
|
328
|
+
*/
|
|
329
|
+
preTransformInstallWorkflow(args) {
|
|
330
|
+
let templates = args.workflowTemplates;
|
|
331
|
+
// Fix: single object instead of array
|
|
332
|
+
if (!Array.isArray(templates)) {
|
|
333
|
+
templates = [templates];
|
|
334
|
+
}
|
|
335
|
+
// Transform each template
|
|
336
|
+
templates = templates.map((template, tIdx) => {
|
|
337
|
+
const result = { ...template };
|
|
338
|
+
// Fix: fields as array instead of object
|
|
339
|
+
if (Array.isArray(template.fields)) {
|
|
340
|
+
const fieldsObj = {};
|
|
341
|
+
template.fields.forEach((field, idx) => {
|
|
342
|
+
const fieldId = `_${1000 + idx}`;
|
|
343
|
+
const { name, ...rest } = field;
|
|
344
|
+
fieldsObj[fieldId] = { ...rest, label: rest.label || name };
|
|
345
|
+
});
|
|
346
|
+
result.fields = fieldsObj;
|
|
347
|
+
}
|
|
348
|
+
else if (template.fields) {
|
|
349
|
+
// Fix: field IDs not matching pattern
|
|
350
|
+
const fieldsObj = {};
|
|
351
|
+
let idx = 0;
|
|
352
|
+
for (const [key, field] of Object.entries(template.fields)) {
|
|
353
|
+
const fieldId = /^_\d{4}$/.test(key) ? key : `_${1000 + idx}`;
|
|
354
|
+
const f = field;
|
|
355
|
+
const { name, ...rest } = f;
|
|
356
|
+
fieldsObj[fieldId] = { ...rest, label: rest.label || name || key };
|
|
357
|
+
idx++;
|
|
358
|
+
}
|
|
359
|
+
result.fields = fieldsObj;
|
|
360
|
+
}
|
|
361
|
+
// Fix: phases as array instead of object
|
|
362
|
+
if (Array.isArray(template.phases)) {
|
|
363
|
+
const phasesObj = {};
|
|
364
|
+
template.phases.forEach((phase, idx) => {
|
|
365
|
+
const phaseId = `_${2000 + idx}`;
|
|
366
|
+
phasesObj[phaseId] = { name: phase.name };
|
|
367
|
+
});
|
|
368
|
+
result.phases = phasesObj;
|
|
369
|
+
}
|
|
370
|
+
else if (template.phases) {
|
|
371
|
+
// Fix: phase IDs not matching pattern
|
|
372
|
+
const phasesObj = {};
|
|
373
|
+
let idx = 0;
|
|
374
|
+
for (const [key, phase] of Object.entries(template.phases)) {
|
|
375
|
+
const phaseId = /^_\d{4}$/.test(key) ? key : `_${2000 + idx}`;
|
|
376
|
+
const p = phase;
|
|
377
|
+
phasesObj[phaseId] = { name: p.name || key };
|
|
378
|
+
idx++;
|
|
379
|
+
}
|
|
380
|
+
result.phases = phasesObj;
|
|
381
|
+
}
|
|
382
|
+
// Fix: wrong field types
|
|
383
|
+
if (result.fields) {
|
|
384
|
+
for (const fieldId of Object.keys(result.fields)) {
|
|
385
|
+
const f = result.fields[fieldId];
|
|
386
|
+
// select ā textpredefinedoptions
|
|
387
|
+
if (f.type === 'select')
|
|
388
|
+
f.type = 'textpredefinedoptions';
|
|
389
|
+
// user ā users
|
|
390
|
+
if (f.type === 'user')
|
|
391
|
+
f.type = 'users';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
});
|
|
396
|
+
return { ...args, workflowTemplates: templates };
|
|
397
|
+
}
|
|
321
398
|
/**
|
|
322
399
|
* Get tool metadata (for access control checks)
|
|
323
400
|
*/
|
|
@@ -658,6 +658,16 @@ exports.createActivityTool = {
|
|
|
658
658
|
}),
|
|
659
659
|
async execute(args, context) {
|
|
660
660
|
try {
|
|
661
|
+
// DEBUG: Log raw input to diagnose LLM tool calling issues
|
|
662
|
+
logger.info("create_activity called", {
|
|
663
|
+
hasName: !!args.name,
|
|
664
|
+
hasActivities: !!args.activities,
|
|
665
|
+
activitiesType: typeof args.activities,
|
|
666
|
+
activitiesIsArray: Array.isArray(args.activities),
|
|
667
|
+
activitiesLength: Array.isArray(args.activities) ? args.activities.length :
|
|
668
|
+
(typeof args.activities === 'string' ? args.activities.length : 0),
|
|
669
|
+
rawArgs: JSON.stringify(args, null, 2).slice(0, 500), // First 500 chars to avoid huge logs
|
|
670
|
+
});
|
|
661
671
|
// Helper function to process a single activity object
|
|
662
672
|
const processActivity = (activityData) => {
|
|
663
673
|
const activity = {
|
|
@@ -797,6 +807,43 @@ exports.createActivityTool = {
|
|
|
797
807
|
options.discussionId = args.discussionId;
|
|
798
808
|
}
|
|
799
809
|
}
|
|
810
|
+
// Pre-validate required fields before API call
|
|
811
|
+
const workflow = context.init.processes?.find((p) => p._id === args.workflowId);
|
|
812
|
+
if (workflow?.fields) {
|
|
813
|
+
const requiredFields = [];
|
|
814
|
+
for (const [fieldId, field] of Object.entries(workflow.fields)) {
|
|
815
|
+
const f = field;
|
|
816
|
+
if (f.required) {
|
|
817
|
+
requiredFields.push({
|
|
818
|
+
id: fieldId,
|
|
819
|
+
label: f.label || f.key || fieldId,
|
|
820
|
+
key: f.key || fieldId,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (requiredFields.length > 0) {
|
|
825
|
+
const missingByActivity = [];
|
|
826
|
+
for (let i = 0; i < activitiesToCreate.length; i++) {
|
|
827
|
+
const activity = activitiesToCreate[i];
|
|
828
|
+
const activityFields = activity.fields || {};
|
|
829
|
+
const missing = requiredFields.filter(rf => activityFields[rf.id] === undefined &&
|
|
830
|
+
activityFields[rf.key] === undefined);
|
|
831
|
+
if (missing.length > 0) {
|
|
832
|
+
const activityName = activity.name || `Activity ${i + 1}`;
|
|
833
|
+
missingByActivity.push(`${activityName}: missing ${missing.map(m => `"${m.label}" (${m.key})`).join(', ')}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (missingByActivity.length > 0) {
|
|
837
|
+
const fieldList = requiredFields.map(f => `- ${f.label} (key: ${f.key}, id: ${f.id})`).join('\n');
|
|
838
|
+
return {
|
|
839
|
+
content: [{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: `ā Missing required fields - cannot create activities.\n\n**Required fields for this workflow:**\n${fieldList}\n\n**Activities with missing fields:**\n${missingByActivity.map(m => `- ${m}`).join('\n')}\n\nš” Add the missing fields to each activity's \`fields\` object using either the field key or ID.`,
|
|
842
|
+
}],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
800
847
|
// Call API
|
|
801
848
|
const result = await context.hailer.createActivities(args.workflowId, activitiesToCreate, options);
|
|
802
849
|
// Handle response
|
|
@@ -14,6 +14,7 @@ const zod_1 = require("zod");
|
|
|
14
14
|
const tool_registry_1 = require("../tool-registry");
|
|
15
15
|
const index_1 = require("../utils/index");
|
|
16
16
|
const workspace_cache_1 = require("../workspace-cache");
|
|
17
|
+
const hailer_clients_1 = require("../hailer-clients");
|
|
17
18
|
const logger = (0, index_1.createLogger)({ component: 'discussion-tools' });
|
|
18
19
|
// ============================================================================
|
|
19
20
|
// HELPER FUNCTIONS
|
|
@@ -786,7 +787,17 @@ exports.joinDiscussionTool = {
|
|
|
786
787
|
// ============================================================================
|
|
787
788
|
// TOOL 5: LEAVE DISCUSSION
|
|
788
789
|
// ============================================================================
|
|
789
|
-
const leaveDiscussionDescription = `Leave a discussion -
|
|
790
|
+
const leaveDiscussionDescription = `Leave a discussion - Bot removes itself from a discussion.
|
|
791
|
+
|
|
792
|
+
**Parameters (at least one required):**
|
|
793
|
+
- \`discussionId\`: The discussion ID to leave (preferred for DMs and general discussions)
|
|
794
|
+
- \`activityId\`: The activity ID whose discussion to leave (for activity-linked discussions)
|
|
795
|
+
|
|
796
|
+
**Examples:**
|
|
797
|
+
- Leave by discussion ID: \`leave_discussion({ discussionId: "69525ce11d392644b7323050" })\`
|
|
798
|
+
- Leave by activity ID: \`leave_discussion({ activityId: "69525ce11d392644b732304f" })\`
|
|
799
|
+
|
|
800
|
+
**Note:** You MUST provide at least one parameter. The parameter cannot be inferred automatically.`;
|
|
790
801
|
exports.leaveDiscussionTool = {
|
|
791
802
|
name: 'leave_discussion',
|
|
792
803
|
group: tool_registry_1.ToolGroup.WRITE,
|
|
@@ -806,6 +817,14 @@ exports.leaveDiscussionTool = {
|
|
|
806
817
|
message: "Either activityId or discussionId must be provided"
|
|
807
818
|
}),
|
|
808
819
|
async execute(args, context) {
|
|
820
|
+
// DEBUG: Log raw input to diagnose LLM tool calling issues
|
|
821
|
+
logger.info("leave_discussion called", {
|
|
822
|
+
hasActivityId: !!args.activityId,
|
|
823
|
+
hasDiscussionId: !!args.discussionId,
|
|
824
|
+
activityId: args.activityId || '(not provided)',
|
|
825
|
+
discussionId: args.discussionId || '(not provided)',
|
|
826
|
+
rawArgs: JSON.stringify(args, null, 2),
|
|
827
|
+
});
|
|
809
828
|
logger.debug('Leaving discussion', {
|
|
810
829
|
activityId: args.activityId,
|
|
811
830
|
discussionId: args.discussionId,
|
|
@@ -969,6 +988,30 @@ exports.inviteDiscussionMembersTool = {
|
|
|
969
988
|
};
|
|
970
989
|
}
|
|
971
990
|
const existingMembers = new Set(discussion?.participants || []);
|
|
991
|
+
// Ensure the bot is a member before inviting others
|
|
992
|
+
// This allows the bot to post messages after inviting users
|
|
993
|
+
try {
|
|
994
|
+
const botUserId = await (0, hailer_clients_1.getCurrentUserId)(context.client);
|
|
995
|
+
if (botUserId && !existingMembers.has(botUserId)) {
|
|
996
|
+
try {
|
|
997
|
+
await context.hailer.joinDiscussion(args.discussionId);
|
|
998
|
+
logger.debug('Bot joined discussion before inviting', { discussionId: args.discussionId });
|
|
999
|
+
}
|
|
1000
|
+
catch (joinError) {
|
|
1001
|
+
// Non-fatal: bot might not be able to join some discussion types
|
|
1002
|
+
logger.debug('Bot join before invite failed (non-fatal)', {
|
|
1003
|
+
discussionId: args.discussionId,
|
|
1004
|
+
error: joinError instanceof Error ? joinError.message : String(joinError)
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
catch (userIdError) {
|
|
1010
|
+
// Non-fatal: if we can't get the bot's user ID, continue with the invite
|
|
1011
|
+
logger.debug('Could not get bot user ID (non-fatal)', {
|
|
1012
|
+
error: userIdError instanceof Error ? userIdError.message : String(userIdError)
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
972
1015
|
// Filter out users who are already members
|
|
973
1016
|
const alreadyMembers = [];
|
|
974
1017
|
const newUsers = [];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Victoria Metrics Tools - Query and List Metrics
|
|
3
|
+
*
|
|
4
|
+
* Tools for querying Hailer metrics from Victoria Metrics:
|
|
5
|
+
* - Query metrics with PromQL (READ)
|
|
6
|
+
* - List available metrics (READ)
|
|
7
|
+
*/
|
|
8
|
+
import { Tool } from '../tool-registry';
|
|
9
|
+
export declare const queryMetricTool: Tool;
|
|
10
|
+
export declare const listMetricsTool: Tool;
|
|
11
|
+
export declare const searchWorkspaceForMetricsTool: Tool;
|
|
12
|
+
export declare const searchUserForMetricsTool: Tool;
|
|
13
|
+
//# sourceMappingURL=metrics.d.ts.map
|