@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.
Files changed (40) hide show
  1. package/.claude/skills/client-bot-architecture/skill.md +340 -0
  2. package/.claude/skills/publish-hailer-app/SKILL.md +11 -0
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +116 -84
  5. package/dist/bot/chat-bot.d.ts +31 -0
  6. package/dist/bot/chat-bot.js +356 -0
  7. package/dist/cli.d.ts +9 -1
  8. package/dist/cli.js +71 -2
  9. package/dist/config.d.ts +15 -2
  10. package/dist/config.js +53 -3
  11. package/dist/lib/logger.js +11 -11
  12. package/dist/mcp/hailer-clients.js +12 -11
  13. package/dist/mcp/tool-registry.d.ts +4 -0
  14. package/dist/mcp/tool-registry.js +78 -1
  15. package/dist/mcp/tools/activity.js +47 -0
  16. package/dist/mcp/tools/discussion.js +44 -1
  17. package/dist/mcp/tools/metrics.d.ts +13 -0
  18. package/dist/mcp/tools/metrics.js +546 -0
  19. package/dist/mcp/tools/user.d.ts +1 -0
  20. package/dist/mcp/tools/user.js +94 -1
  21. package/dist/mcp/tools/workflow.js +109 -40
  22. package/dist/mcp/webhook-handler.js +7 -4
  23. package/dist/mcp-server.js +22 -6
  24. package/dist/stdio-server.d.ts +14 -0
  25. package/dist/stdio-server.js +101 -0
  26. package/package.json +6 -6
  27. package/scripts/test-hal-tools.ts +154 -0
  28. package/test-billing-server.js +136 -0
  29. package/dist/lib/discussion-lock.d.ts +0 -42
  30. package/dist/lib/discussion-lock.js +0 -110
  31. package/dist/mcp/tools/bot-config/constants.d.ts +0 -23
  32. package/dist/mcp/tools/bot-config/constants.js +0 -94
  33. package/dist/mcp/tools/bot-config/core.d.ts +0 -253
  34. package/dist/mcp/tools/bot-config/core.js +0 -2456
  35. package/dist/mcp/tools/bot-config/index.d.ts +0 -10
  36. package/dist/mcp/tools/bot-config/index.js +0 -59
  37. package/dist/mcp/tools/bot-config/tools.d.ts +0 -7
  38. package/dist/mcp/tools/bot-config/tools.js +0 -15
  39. package/dist/mcp/tools/bot-config/types.d.ts +0 -50
  40. 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: args.workflowTemplates.length
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
- args.workflowTemplates
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
- let responseText = `✅ **Workflow Installation Successful**\n\n`;
470
- responseText += `**Workspace:** ${workspaceId}\n`;
471
- responseText += `**Workflows Installed:** ${args.workflowTemplates.length}\n\n`;
472
- // Show workflow names and their real IDs
473
- const templates = args.workflowTemplates;
474
- const workflowMappings = templates
475
- .filter((wf) => wf._id)
476
- .map((wf) => `- ${wf.name}: \`${result[wf._id]}\` (from workflow _id \`${wf._id}\`)`)
477
- .join('\n');
478
- if (workflowMappings) {
479
- responseText += `**Created Workflows:**\n${workflowMappings}\n\n`;
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 += `**Complete ID Mapping (template_id real_id):**\n`;
482
- responseText += `\`\`\`json\n${JSON.stringify(result, null, 2)}\n\`\`\`\n\n`;
483
- responseText += `💡 **Next Steps:**\n`;
484
- responseText += `- Use \`list_workflows\` to see the new workflows in context\n`;
485
- responseText += `- Use \`create_activity\` with the real workflow IDs to create activities\n`;
486
- responseText += `- Field IDs from mapping are used when setting activity field values\n\n`;
487
- responseText += `**Example Activity Creation:**\n`;
488
- responseText += `\`\`\`javascript\n`;
489
- if (templates[0]?._id) {
490
- const firstTemplate = templates[0];
491
- const workflowId = result[firstTemplate._id];
492
- const firstFieldId = Object.keys(firstTemplate.fields || {})[0];
493
- const realFieldId = firstFieldId ? result[firstFieldId] : 'field_id';
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
- else {
502
- responseText += `create_activity({\n`;
503
- responseText += ` workflowId: result._0001,\n`;
504
- responseText += ` name: "New Activity",\n`;
505
- responseText += ` fields: { [result._1000]: "value" }\n`;
506
- responseText += `});\n`;
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
- if (a.length !== b.length) {
66
- // Still perform comparison to maintain constant time
67
- crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
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(Buffer.from(a), Buffer.from(b));
73
+ return crypto.timingSafeEqual(bufA, bufB);
71
74
  }
72
75
  /**
73
76
  * Generate HMAC-SHA256 signature for webhook payload
@@ -121,7 +121,7 @@ class MCPServerService {
121
121
  status: 'ok',
122
122
  timestamp: new Date().toISOString(),
123
123
  service: 'hailer-mcp-server',
124
- version: '0.1.0'
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 endpoint - JSON-RPC 2.0 over SSE
145
- this.app.post('/api/mcp', async (req, res) => {
146
- req.logger.debug('MCP request received', { method: req.body?.method });
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: '1.0.0'
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.2.7",
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);