@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
|
@@ -388,7 +388,7 @@ const installWorkflowSchema = zod_1.z.object({
|
|
|
388
388
|
]).describe("Field type"),
|
|
389
389
|
key: zod_1.z.string().optional().describe("Readable field name (like SQL column name) - RECOMMENDED"),
|
|
390
390
|
required: zod_1.z.boolean().optional().describe("Whether field is required"),
|
|
391
|
-
data: zod_1.z.array(zod_1.z.string()).optional().describe("For textpredefinedoptions: string array like [\"Low\", \"Medium\", \"High\"]. For activitylink: target workflow IDs"),
|
|
391
|
+
data: zod_1.z.array(zod_1.z.string()).optional().describe("For textpredefinedoptions: string array like [\"Low\", \"Medium\", \"High\"]. For activitylink: target workflow IDs. NOTE: 'options' also accepted, will be converted to 'data'"),
|
|
392
392
|
placeholder: zod_1.z.string().optional().describe("Placeholder text"),
|
|
393
393
|
unit: zod_1.z.string().optional().describe("Unit for numeric/textunit fields"),
|
|
394
394
|
description: zod_1.z.string().optional().describe("Field description"),
|
|
@@ -441,14 +441,61 @@ exports.installWorkflowTool = {
|
|
|
441
441
|
}],
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
|
+
// Transform templates: fix common LLM mistakes
|
|
445
|
+
const transformedTemplates = args.workflowTemplates.map((template) => {
|
|
446
|
+
let result = { ...template };
|
|
447
|
+
// Transform fields
|
|
448
|
+
if (template.fields) {
|
|
449
|
+
const transformedFields = {};
|
|
450
|
+
for (const [fieldId, field] of Object.entries(template.fields)) {
|
|
451
|
+
const f = { ...field };
|
|
452
|
+
// Convert options/predefinedOptions/selectOptions → data
|
|
453
|
+
if (!f.data) {
|
|
454
|
+
if (f.options) {
|
|
455
|
+
f.data = f.options;
|
|
456
|
+
delete f.options;
|
|
457
|
+
}
|
|
458
|
+
else if (f.predefinedOptions) {
|
|
459
|
+
f.data = f.predefinedOptions;
|
|
460
|
+
delete f.predefinedOptions;
|
|
461
|
+
}
|
|
462
|
+
else if (f.selectOptions) {
|
|
463
|
+
// Handle both array and object selectOptions
|
|
464
|
+
f.data = Array.isArray(f.selectOptions)
|
|
465
|
+
? f.selectOptions.map((o) => o.label || o.value || o)
|
|
466
|
+
: Object.values(f.selectOptions);
|
|
467
|
+
delete f.selectOptions;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Remove disallowed properties
|
|
471
|
+
delete f.name; // Only 'label' is allowed
|
|
472
|
+
delete f.sequence; // Not supported
|
|
473
|
+
delete f.order; // Not supported
|
|
474
|
+
transformedFields[fieldId] = f;
|
|
475
|
+
}
|
|
476
|
+
result.fields = transformedFields;
|
|
477
|
+
}
|
|
478
|
+
// Transform phases - remove sequence/order
|
|
479
|
+
if (template.phases) {
|
|
480
|
+
const transformedPhases = {};
|
|
481
|
+
for (const [phaseId, phase] of Object.entries(template.phases)) {
|
|
482
|
+
const p = { ...phase };
|
|
483
|
+
delete p.sequence;
|
|
484
|
+
delete p.order;
|
|
485
|
+
transformedPhases[phaseId] = p;
|
|
486
|
+
}
|
|
487
|
+
result.phases = transformedPhases;
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
});
|
|
444
491
|
logger.debug('Calling v3.workflow.install', {
|
|
445
492
|
workspaceId,
|
|
446
|
-
templatesCount:
|
|
493
|
+
templatesCount: transformedTemplates.length
|
|
447
494
|
});
|
|
448
495
|
// Call v3.workflow.install endpoint
|
|
449
496
|
const result = await context.hailer.request('v3.workflow.install', [
|
|
450
497
|
workspaceId,
|
|
451
|
-
|
|
498
|
+
transformedTemplates
|
|
452
499
|
]);
|
|
453
500
|
logger.debug('Workflow installation successful', { result });
|
|
454
501
|
// Refresh context.init.processes cache so new workflows are immediately available
|
|
@@ -465,46 +512,68 @@ exports.installWorkflowTool = {
|
|
|
465
512
|
logger.warn('Failed to refresh workflow cache after installation', { error: refreshError });
|
|
466
513
|
// Non-fatal - the workflow was created, just cache is stale
|
|
467
514
|
}
|
|
468
|
-
// Build success response
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
515
|
+
// Build success response - clearly separate workflow/field/phase IDs
|
|
516
|
+
const templates = transformedTemplates;
|
|
517
|
+
// Extract workflow ID (the one that starts with workflow template ID or is for the workflow)
|
|
518
|
+
// The result maps template IDs to real IDs
|
|
519
|
+
const workflowIds = [];
|
|
520
|
+
const fieldIds = {};
|
|
521
|
+
const phaseIds = {};
|
|
522
|
+
for (const [templateId, realId] of Object.entries(result)) {
|
|
523
|
+
if (templateId.startsWith('_0') || templateId.startsWith('_00')) {
|
|
524
|
+
// Workflow ID (_0001, _0002, etc.)
|
|
525
|
+
workflowIds.push(realId);
|
|
526
|
+
}
|
|
527
|
+
else if (templateId.startsWith('_1')) {
|
|
528
|
+
// Field ID (_1000, _1001, etc.)
|
|
529
|
+
fieldIds[templateId] = realId;
|
|
530
|
+
}
|
|
531
|
+
else if (templateId.startsWith('_2')) {
|
|
532
|
+
// Phase ID (_2000, _2001, etc.)
|
|
533
|
+
phaseIds[templateId] = realId;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Fallback - assume workflow ID if no underscore pattern
|
|
537
|
+
workflowIds.push(realId);
|
|
538
|
+
}
|
|
480
539
|
}
|
|
481
|
-
responseText
|
|
482
|
-
|
|
483
|
-
responseText +=
|
|
484
|
-
responseText +=
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
responseText += `// Using workflow: ${firstTemplate.name}\n`;
|
|
495
|
-
responseText += `create_activity({\n`;
|
|
496
|
-
responseText += ` workflowId: "${workflowId}",\n`;
|
|
497
|
-
responseText += ` name: "New Activity",\n`;
|
|
498
|
-
responseText += ` fields: { "${realFieldId}": "value" }\n`;
|
|
499
|
-
responseText += `});\n`;
|
|
540
|
+
let responseText = `✅ **Workflow "${templates[0]?.name || 'Unknown'}" Created**\n\n`;
|
|
541
|
+
// MOST IMPORTANT - Workflow ID for create_activity
|
|
542
|
+
responseText += `**🎯 WORKFLOW ID (use this for create_activity):**\n`;
|
|
543
|
+
responseText += `\`${workflowIds[0] || 'check list_workflows'}\`\n\n`;
|
|
544
|
+
// Field IDs
|
|
545
|
+
if (Object.keys(fieldIds).length > 0) {
|
|
546
|
+
responseText += `**📋 Field IDs:**\n`;
|
|
547
|
+
for (const [tpl, real] of Object.entries(fieldIds)) {
|
|
548
|
+
const fieldDef = templates[0]?.fields?.[tpl];
|
|
549
|
+
const label = fieldDef?.label || tpl;
|
|
550
|
+
responseText += `- ${label}: \`${real}\`\n`;
|
|
551
|
+
}
|
|
552
|
+
responseText += `\n`;
|
|
500
553
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
responseText +=
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
554
|
+
// Phase IDs
|
|
555
|
+
if (Object.keys(phaseIds).length > 0) {
|
|
556
|
+
responseText += `**🏷️ Phase IDs:**\n`;
|
|
557
|
+
for (const [tpl, real] of Object.entries(phaseIds)) {
|
|
558
|
+
const phaseDef = templates[0]?.phases?.[tpl];
|
|
559
|
+
const name = phaseDef?.name || tpl;
|
|
560
|
+
responseText += `- ${name}: \`${real}\`\n`;
|
|
561
|
+
}
|
|
562
|
+
responseText += `\n`;
|
|
507
563
|
}
|
|
564
|
+
// Example
|
|
565
|
+
const firstFieldId = Object.values(fieldIds)[0];
|
|
566
|
+
const firstPhaseId = Object.values(phaseIds)[0];
|
|
567
|
+
responseText += `**Example create_activity:**\n`;
|
|
568
|
+
responseText += `\`\`\`json\n`;
|
|
569
|
+
responseText += `{\n`;
|
|
570
|
+
responseText += ` "workflowId": "${workflowIds[0] || 'WORKFLOW_ID'}",\n`;
|
|
571
|
+
responseText += ` "name": "New Activity",\n`;
|
|
572
|
+
if (firstPhaseId)
|
|
573
|
+
responseText += ` "phaseId": "${firstPhaseId}",\n`;
|
|
574
|
+
if (firstFieldId)
|
|
575
|
+
responseText += ` "fields": { "${firstFieldId}": "value" }\n`;
|
|
576
|
+
responseText += `}\n`;
|
|
508
577
|
responseText += `\`\`\``;
|
|
509
578
|
return {
|
|
510
579
|
content: [{
|
|
@@ -62,12 +62,15 @@ const WEBHOOK_SECRET_FILE = 'webhook-secret.txt';
|
|
|
62
62
|
* Constant-time string comparison to prevent timing attacks
|
|
63
63
|
*/
|
|
64
64
|
function timingSafeEqual(a, b) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const bufA = Buffer.from(a);
|
|
66
|
+
const bufB = Buffer.from(b);
|
|
67
|
+
if (bufA.length !== bufB.length) {
|
|
68
|
+
// Compare against dummy buffer of same length to maintain constant time
|
|
69
|
+
const dummy = Buffer.alloc(bufA.length);
|
|
70
|
+
crypto.timingSafeEqual(bufA, dummy);
|
|
68
71
|
return false;
|
|
69
72
|
}
|
|
70
|
-
return crypto.timingSafeEqual(
|
|
73
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
76
|
* Generate HMAC-SHA256 signature for webhook payload
|
package/dist/mcp-server.js
CHANGED
|
@@ -121,7 +121,7 @@ class MCPServerService {
|
|
|
121
121
|
status: 'ok',
|
|
122
122
|
timestamp: new Date().toISOString(),
|
|
123
123
|
service: 'hailer-mcp-server',
|
|
124
|
-
version:
|
|
124
|
+
version: config_1.APP_VERSION
|
|
125
125
|
};
|
|
126
126
|
res.json(health);
|
|
127
127
|
});
|
|
@@ -141,11 +141,11 @@ class MCPServerService {
|
|
|
141
141
|
daemons: status
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
-
// MCP Protocol
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// MCP Protocol handler - shared by both routes
|
|
145
|
+
const mcpHandler = async (req, res, apiKeyOverride) => {
|
|
146
|
+
const apiKey = apiKeyOverride || req.query.apiKey;
|
|
147
|
+
req.logger.debug('MCP request received', { method: req.body?.method, apiKey: apiKey?.slice(0, 8) + '...' });
|
|
147
148
|
try {
|
|
148
|
-
const apiKey = req.query.apiKey;
|
|
149
149
|
const mcpRequest = req.body;
|
|
150
150
|
let result;
|
|
151
151
|
if (mcpRequest.method === 'tools/list') {
|
|
@@ -225,7 +225,7 @@ class MCPServerService {
|
|
|
225
225
|
capabilities: { tools: {} },
|
|
226
226
|
serverInfo: {
|
|
227
227
|
name: 'hailer-mcp-server',
|
|
228
|
-
version:
|
|
228
|
+
version: config_1.APP_VERSION
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
}
|
|
@@ -264,6 +264,22 @@ class MCPServerService {
|
|
|
264
264
|
this.sendMcpError(res, req.body?.id || null, -32000, `Server error: ${errorMessage}`, 500);
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
+
};
|
|
268
|
+
// MCP Protocol endpoint - JSON-RPC 2.0 over SSE
|
|
269
|
+
// Route 1: /api/mcp?apiKey=xxx (standard format)
|
|
270
|
+
this.app.post('/api/mcp', (req, res) => mcpHandler(req, res));
|
|
271
|
+
// Route 2: /:apiKey (simplified format - API key as path)
|
|
272
|
+
// Matches 16-64 char alphanumeric keys, but ONLY for MCP requests (has jsonrpc field)
|
|
273
|
+
// Non-MCP requests (webhooks) pass through to later routes
|
|
274
|
+
this.app.post('/:apiKey([a-zA-Z0-9_-]{16,64})', (req, res, next) => {
|
|
275
|
+
if (req.body?.jsonrpc) {
|
|
276
|
+
// MCP request - handle it
|
|
277
|
+
mcpHandler(req, res, req.params.apiKey);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Not MCP (likely webhook) - pass to next route
|
|
281
|
+
next();
|
|
282
|
+
}
|
|
267
283
|
});
|
|
268
284
|
// ===== Bot Configuration API (only when MCP_CLIENT_ENABLED=true) =====
|
|
269
285
|
if (config_1.environment.MCP_CLIENT_ENABLED) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio MCP Server for Claude Desktop
|
|
3
|
+
*
|
|
4
|
+
* Uses @modelcontextprotocol/sdk with StdioServerTransport for
|
|
5
|
+
* JSON-RPC communication via stdin/stdout.
|
|
6
|
+
*
|
|
7
|
+
* This is the entry point when running as Claude Desktop MCP connector.
|
|
8
|
+
*/
|
|
9
|
+
import { ToolRegistry } from './mcp/tool-registry';
|
|
10
|
+
/**
|
|
11
|
+
* Start the stdio MCP server with all registered tools
|
|
12
|
+
*/
|
|
13
|
+
export declare function startStdioServer(toolRegistry: ToolRegistry): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=stdio-server.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stdio MCP Server for Claude Desktop
|
|
4
|
+
*
|
|
5
|
+
* Uses @modelcontextprotocol/sdk with StdioServerTransport for
|
|
6
|
+
* JSON-RPC communication via stdin/stdout.
|
|
7
|
+
*
|
|
8
|
+
* This is the entry point when running as Claude Desktop MCP connector.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.startStdioServer = startStdioServer;
|
|
12
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
13
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
14
|
+
const logger_1 = require("./lib/logger");
|
|
15
|
+
const tool_registry_1 = require("./mcp/tool-registry");
|
|
16
|
+
const UserContextCache_1 = require("./mcp/UserContextCache");
|
|
17
|
+
const config_1 = require("./config");
|
|
18
|
+
const logger = (0, logger_1.createLogger)({ component: 'stdio-server' });
|
|
19
|
+
/**
|
|
20
|
+
* Start the stdio MCP server with all registered tools
|
|
21
|
+
*/
|
|
22
|
+
async function startStdioServer(toolRegistry) {
|
|
23
|
+
logger.info('Starting stdio MCP server for Claude Desktop');
|
|
24
|
+
// Get API key from environment (set in Claude Desktop config)
|
|
25
|
+
const apiKey = process.env.MCP_CLIENT_API_KEY;
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
logger.error('MCP_CLIENT_API_KEY environment variable is required for stdio mode');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Create MCP server
|
|
31
|
+
const server = new mcp_js_1.McpServer({
|
|
32
|
+
name: 'hailer-mcp-server',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
});
|
|
35
|
+
// Get tool definitions (filtered by allowed groups, excluding NUCLEAR unless enabled)
|
|
36
|
+
const allowedGroups = config_1.environment.ENABLE_NUCLEAR_TOOLS
|
|
37
|
+
? [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.NUCLEAR]
|
|
38
|
+
: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND];
|
|
39
|
+
const toolDefinitions = toolRegistry.getToolDefinitions({ allowedGroups });
|
|
40
|
+
logger.info('Registering tools for stdio server', {
|
|
41
|
+
toolCount: toolDefinitions.length,
|
|
42
|
+
allowedGroups,
|
|
43
|
+
});
|
|
44
|
+
// Register each tool with the MCP server
|
|
45
|
+
for (const toolDef of toolDefinitions) {
|
|
46
|
+
server.tool(toolDef.name, toolDef.description, toolDef.inputSchema.properties ? toolDef.inputSchema : { type: 'object', properties: {} }, async (args) => {
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
logger.debug('Tool call received', { toolName: toolDef.name, args });
|
|
49
|
+
try {
|
|
50
|
+
// Get user context (cached)
|
|
51
|
+
const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
|
|
52
|
+
// Execute the tool
|
|
53
|
+
const result = await toolRegistry.executeTool(toolDef.name, args, userContext);
|
|
54
|
+
const duration = Date.now() - startTime;
|
|
55
|
+
logger.info('Tool call completed', { toolName: toolDef.name, duration });
|
|
56
|
+
// Handle different result formats
|
|
57
|
+
if (result && typeof result === 'object' && 'content' in result) {
|
|
58
|
+
// Result is already in MCP format { content: [...] }
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
// Wrap result in MCP format
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
66
|
+
}]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const duration = Date.now() - startTime;
|
|
71
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
72
|
+
logger.error('Tool call failed', error, { toolName: toolDef.name, duration });
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `Error: ${errorMessage}`
|
|
77
|
+
}],
|
|
78
|
+
isError: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Create stdio transport
|
|
84
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
85
|
+
// Handle graceful shutdown
|
|
86
|
+
process.on('SIGINT', async () => {
|
|
87
|
+
logger.info('Received SIGINT, shutting down stdio server');
|
|
88
|
+
await server.close();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
});
|
|
91
|
+
process.on('SIGTERM', async () => {
|
|
92
|
+
logger.info('Received SIGTERM, shutting down stdio server');
|
|
93
|
+
await server.close();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
});
|
|
96
|
+
// Connect and start serving
|
|
97
|
+
logger.info('Connecting stdio transport');
|
|
98
|
+
await server.connect(transport);
|
|
99
|
+
logger.info('Stdio MCP server is running');
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=stdio-server.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hailer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.21",
|
|
4
4
|
"config": {
|
|
5
5
|
"docker": {
|
|
6
6
|
"registry": "registry.gitlab.com/hailer-repos/hailer-mcp"
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"start": "node dist/app.js",
|
|
16
16
|
"lint": "eslint src/",
|
|
17
|
-
"build-dev-push": "rm -rf build/docker-dev && docker build --target artifacts --output build/docker-dev . -f Dockerfile.build-dev && docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs)-dev -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):dev -f ./Dockerfile.dev .",
|
|
18
|
-
"build-debug-push": "docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs)-debug -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):debug -f ./Dockerfile.debug .",
|
|
19
|
-
"build-prod-push": "rm -rf build/docker-prod && docker build --target artifacts --output build/docker-prod . -f Dockerfile.build-prod && docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs) -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):prod -f ./Dockerfile.prod .",
|
|
20
|
-
"build-k3d": "docker build -f Dockerfile.debug . -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs) && k3d image import $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs) -c hailer",
|
|
21
|
-
"build-k3d-dummy": "docker build --target artifacts --output build/docker-dev . -f Dockerfile.build-dev && docker build -f Dockerfile.dev . -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs)-dev && k3d image import $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs):$(npm pkg get version | xargs)-dev -c hailer",
|
|
17
|
+
"build-dev-push": "rm -rf build/docker-dev && docker build --target artifacts --output build/docker-dev . -f Dockerfile.build-dev && docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs)-dev -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):dev -f ./Dockerfile.dev .",
|
|
18
|
+
"build-debug-push": "docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs)-debug -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):debug -f ./Dockerfile.debug .",
|
|
19
|
+
"build-prod-push": "rm -rf build/docker-prod && docker build --target artifacts --output build/docker-prod . -f Dockerfile.build-prod && docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs) -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):prod -f ./Dockerfile.prod .",
|
|
20
|
+
"build-k3d": "docker build -f Dockerfile.debug . -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs) && k3d image import $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs) -c hailer",
|
|
21
|
+
"build-k3d-dummy": "docker build --target artifacts --output build/docker-dev . -f Dockerfile.build-dev && docker build -f Dockerfile.dev . -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs)-dev && k3d image import $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs | sed 's/@hailer\\//hailer-/'):$(npm pkg get version | xargs)-dev -c hailer",
|
|
22
22
|
"mcp-server": "hailer-mcp",
|
|
23
23
|
"server": "tsx src/client/server.ts",
|
|
24
24
|
"server:prod": "node dist/client/server.js",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* HAL Tools Test - Simple version
|
|
4
|
+
* Just calls each tool and shows the result. No magic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as dotenv from 'dotenv';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env.local') });
|
|
11
|
+
|
|
12
|
+
const MCP_URL = process.env.MCP_SERVER_URL || 'http://localhost:3030/api/mcp';
|
|
13
|
+
|
|
14
|
+
async function call(apiKey: string, tool: string, args: Record<string, unknown> = {}) {
|
|
15
|
+
console.log(`\n>>> ${tool}`);
|
|
16
|
+
console.log(` args: ${JSON.stringify(args)}`);
|
|
17
|
+
|
|
18
|
+
const res = await fetch(`${MCP_URL}?apiKey=${apiKey}`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
jsonrpc: '2.0',
|
|
23
|
+
id: '1',
|
|
24
|
+
method: 'tools/call',
|
|
25
|
+
params: { name: tool, arguments: args },
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
for (const line of text.split('\n')) {
|
|
31
|
+
if (line.startsWith('data: ')) {
|
|
32
|
+
const data = JSON.parse(line.substring(6));
|
|
33
|
+
if (data.error) {
|
|
34
|
+
console.log(`<<< ERROR: ${data.error.message}`);
|
|
35
|
+
return { error: true, data: null };
|
|
36
|
+
}
|
|
37
|
+
const content = data.result?.content?.[0]?.text || '';
|
|
38
|
+
// Show first 500 chars
|
|
39
|
+
console.log(`<<< ${content.substring(0, 500)}${content.length > 500 ? '...' : ''}`);
|
|
40
|
+
return { error: false, content };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log('<<< NO RESPONSE');
|
|
44
|
+
return { error: true, data: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const apiKey = process.env.MCP_CLIENT_API_KEY;
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
console.error('Set MCP_CLIENT_API_KEY in .env.local');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('=== HAL TOOLS TEST ===\n');
|
|
55
|
+
|
|
56
|
+
// Get a workflow to work with
|
|
57
|
+
const wf = await call(apiKey, 'list_workflows_minimal', {});
|
|
58
|
+
const wfMatch = wf.content?.match(/`([a-f0-9]{24})`/);
|
|
59
|
+
const workflowId = wfMatch?.[1];
|
|
60
|
+
console.log(`\n[Using workflowId: ${workflowId}]`);
|
|
61
|
+
|
|
62
|
+
if (!workflowId) {
|
|
63
|
+
console.log('No workflow found, stopping');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get phase
|
|
68
|
+
const ph = await call(apiKey, 'list_workflow_phases', { workflowId });
|
|
69
|
+
const phMatch = ph.content?.match(/`([a-f0-9]{24})`/);
|
|
70
|
+
const phaseId = phMatch?.[1];
|
|
71
|
+
console.log(`\n[Using phaseId: ${phaseId}]`);
|
|
72
|
+
|
|
73
|
+
// Schema
|
|
74
|
+
await call(apiKey, 'get_workflow_schema', { workflowId, phaseId });
|
|
75
|
+
|
|
76
|
+
// List activities
|
|
77
|
+
await call(apiKey, 'list_activities', { workflowId, phaseId, fields: '[]' });
|
|
78
|
+
|
|
79
|
+
// Count
|
|
80
|
+
await call(apiKey, 'count_activities', { workflowId });
|
|
81
|
+
|
|
82
|
+
// Create activity
|
|
83
|
+
const created = await call(apiKey, 'create_activity', {
|
|
84
|
+
workflowId,
|
|
85
|
+
name: `TEST_${Date.now()}`
|
|
86
|
+
});
|
|
87
|
+
const actMatch = created.content?.match(/\*\*ID\*\*:\s*([a-f0-9]{24})/);
|
|
88
|
+
const activityId = actMatch?.[1];
|
|
89
|
+
console.log(`\n[Created activityId: ${activityId}]`);
|
|
90
|
+
|
|
91
|
+
if (activityId) {
|
|
92
|
+
// Show it
|
|
93
|
+
await call(apiKey, 'show_activity_by_id', { activityId });
|
|
94
|
+
|
|
95
|
+
// Update it
|
|
96
|
+
await call(apiKey, 'update_activity', { activityId, name: `UPDATED_${Date.now()}` });
|
|
97
|
+
|
|
98
|
+
// Delete it (this tool doesn't exist yet)
|
|
99
|
+
await call(apiKey, 'delete_activity', { activityId });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Insights
|
|
103
|
+
await call(apiKey, 'list_insights', {});
|
|
104
|
+
|
|
105
|
+
const sources = [{
|
|
106
|
+
name: 'src',
|
|
107
|
+
workflowId,
|
|
108
|
+
fields: [{ meta: 'name', name: 'Name', as: 'name' }]
|
|
109
|
+
}];
|
|
110
|
+
|
|
111
|
+
await call(apiKey, 'preview_insight', { sources, query: 'SELECT * FROM src LIMIT 3' });
|
|
112
|
+
|
|
113
|
+
const ins = await call(apiKey, 'create_insight', {
|
|
114
|
+
name: `TEST_${Date.now()}`,
|
|
115
|
+
sources,
|
|
116
|
+
query: 'SELECT * FROM src LIMIT 3'
|
|
117
|
+
});
|
|
118
|
+
const insMatch = ins.content?.match(/`([a-f0-9]{24})`/);
|
|
119
|
+
const insightId = insMatch?.[1];
|
|
120
|
+
|
|
121
|
+
if (insightId) {
|
|
122
|
+
await call(apiKey, 'get_insight_data', { insightId });
|
|
123
|
+
await call(apiKey, 'remove_insight', { insightId });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Discussions
|
|
127
|
+
await call(apiKey, 'list_my_discussions', {});
|
|
128
|
+
|
|
129
|
+
if (activityId) {
|
|
130
|
+
await call(apiKey, 'join_discussion', { activityId });
|
|
131
|
+
// Get discussion from activity
|
|
132
|
+
const act = await call(apiKey, 'show_activity_by_id', { activityId });
|
|
133
|
+
const discMatch = act.content?.match(/discussion.*?([a-f0-9]{24})/i);
|
|
134
|
+
const discussionId = discMatch?.[1];
|
|
135
|
+
|
|
136
|
+
if (discussionId) {
|
|
137
|
+
await call(apiKey, 'fetch_discussion_messages', { discussionId });
|
|
138
|
+
await call(apiKey, 'add_discussion_message', { discussionId, content: 'TEST' });
|
|
139
|
+
await call(apiKey, 'get_activity_from_discussion', { discussionId });
|
|
140
|
+
await call(apiKey, 'leave_discussion', { discussionId });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Other
|
|
145
|
+
await call(apiKey, 'list_workflows', {});
|
|
146
|
+
await call(apiKey, 'list_apps', {});
|
|
147
|
+
await call(apiKey, 'list_templates', {});
|
|
148
|
+
await call(apiKey, 'get_workspace_balance', {});
|
|
149
|
+
await call(apiKey, 'search_workspace_users', { query: 'test' });
|
|
150
|
+
|
|
151
|
+
console.log('\n=== DONE ===');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main().catch(console.error);
|