@hailer/mcp 1.0.22 → 1.0.24
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/.env.example +7 -26
- package/dist/app.js +10 -40
- package/dist/bot/chat-bot.js +1 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +2 -71
- package/dist/config.d.ts +3 -96
- package/dist/config.js +6 -146
- package/dist/core.d.ts +0 -46
- package/dist/core.js +2 -351
- package/dist/lib/logger.js +2 -8
- package/dist/mcp/UserContextCache.js +10 -13
- package/dist/mcp/hailer-clients.js +11 -12
- package/dist/mcp/signal-handler.js +2 -18
- package/dist/mcp/tool-registry.d.ts +0 -4
- package/dist/mcp/tool-registry.js +1 -78
- package/dist/mcp/tools/activity.js +54 -6
- package/dist/mcp/tools/discussion.js +0 -25
- package/dist/mcp/tools/user.d.ts +4 -2
- package/dist/mcp/tools/user.js +48 -4
- package/dist/mcp/tools/workflow.js +40 -109
- package/dist/mcp/webhook-handler.d.ts +3 -64
- package/dist/mcp/webhook-handler.js +7 -214
- package/dist/mcp-server.d.ts +0 -4
- package/dist/mcp-server.js +18 -229
- package/package.json +9 -10
- package/.claude/skills/client-bot-architecture/skill.md +0 -340
- package/dist/commands/setup.d.ts +0 -11
- package/dist/commands/setup.js +0 -319
- package/scripts/test-hal-tools.ts +0 -154
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Webhook Handler
|
|
3
|
+
* Webhook Handler Utilities
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* local .bot-config/{workspaceId}.json files.
|
|
5
|
+
* Provides webhook token generation and verification for secure endpoints.
|
|
7
6
|
*/
|
|
8
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
8
|
if (k2 === undefined) k2 = k;
|
|
@@ -43,15 +42,10 @@ exports.generateWebhookSignature = generateWebhookSignature;
|
|
|
43
42
|
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
44
43
|
exports.getWebhookToken = getWebhookToken;
|
|
45
44
|
exports.getWebhookPath = getWebhookPath;
|
|
46
|
-
exports.onBotUpdate = onBotUpdate;
|
|
47
|
-
exports.handleBotConfigWebhook = handleBotConfigWebhook;
|
|
48
|
-
exports.getWorkspaceConfig = getWorkspaceConfig;
|
|
49
|
-
exports.listWorkspaceConfigs = listWorkspaceConfigs;
|
|
50
45
|
const fs = __importStar(require("fs"));
|
|
51
46
|
const path = __importStar(require("path"));
|
|
52
47
|
const crypto = __importStar(require("crypto"));
|
|
53
48
|
const logger_1 = require("../lib/logger");
|
|
54
|
-
const config_1 = require("../config");
|
|
55
49
|
const logger = (0, logger_1.createLogger)({ component: 'webhook-handler' });
|
|
56
50
|
const BOT_CONFIG_DIR = '.bot-config';
|
|
57
51
|
const WEBHOOK_SECRET_FILE = 'webhook-secret.txt';
|
|
@@ -62,15 +56,12 @@ const WEBHOOK_SECRET_FILE = 'webhook-secret.txt';
|
|
|
62
56
|
* Constant-time string comparison to prevent timing attacks
|
|
63
57
|
*/
|
|
64
58
|
function timingSafeEqual(a, b) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Compare against dummy buffer of same length to maintain constant time
|
|
69
|
-
const dummy = Buffer.alloc(bufA.length);
|
|
70
|
-
crypto.timingSafeEqual(bufA, dummy);
|
|
59
|
+
if (a.length !== b.length) {
|
|
60
|
+
// Still perform comparison to maintain constant time
|
|
61
|
+
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
|
|
71
62
|
return false;
|
|
72
63
|
}
|
|
73
|
-
return crypto.timingSafeEqual(
|
|
64
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
74
65
|
}
|
|
75
66
|
/**
|
|
76
67
|
* Generate HMAC-SHA256 signature for webhook payload
|
|
@@ -141,208 +132,10 @@ function getWebhookToken() {
|
|
|
141
132
|
return token;
|
|
142
133
|
}
|
|
143
134
|
/**
|
|
144
|
-
* Get the full webhook path with token
|
|
135
|
+
* Get the full webhook path with token
|
|
145
136
|
*/
|
|
146
137
|
function getWebhookPath() {
|
|
147
138
|
const token = getWebhookToken();
|
|
148
139
|
return token ? `/${token}` : null;
|
|
149
140
|
}
|
|
150
|
-
let botUpdateCallback = null;
|
|
151
|
-
function onBotUpdate(callback) {
|
|
152
|
-
botUpdateCallback = callback;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Get field value from webhook payload by key
|
|
156
|
-
*/
|
|
157
|
-
function getFieldValue(fields, key) {
|
|
158
|
-
const field = fields.find((f) => f.key === key);
|
|
159
|
-
return field?.value ?? null;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Load existing workspace config or create empty one
|
|
163
|
-
*/
|
|
164
|
-
function loadWorkspaceConfig(workspaceId) {
|
|
165
|
-
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
166
|
-
const configPath = path.join(configDir, `${workspaceId}.json`);
|
|
167
|
-
if (fs.existsSync(configPath)) {
|
|
168
|
-
try {
|
|
169
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
logger.warn('Failed to load workspace config', { workspaceId, error: String(error) });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
workspaceId,
|
|
177
|
-
workspaceName: workspaceId,
|
|
178
|
-
specialists: [],
|
|
179
|
-
lastSynced: new Date().toISOString(),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Save workspace config to file
|
|
184
|
-
*/
|
|
185
|
-
function saveWorkspaceConfig(config) {
|
|
186
|
-
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
187
|
-
// Ensure directory exists
|
|
188
|
-
if (!fs.existsSync(configDir)) {
|
|
189
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
190
|
-
}
|
|
191
|
-
const configPath = path.join(configDir, `${config.workspaceId}.json`);
|
|
192
|
-
config.lastSynced = new Date().toISOString();
|
|
193
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
194
|
-
logger.info('Saved workspace config', {
|
|
195
|
-
workspaceId: config.workspaceId,
|
|
196
|
-
path: configPath,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Process webhook payload and update workspace config
|
|
201
|
-
*/
|
|
202
|
-
function handleBotConfigWebhook(payload) {
|
|
203
|
-
const workspaceId = payload.cid;
|
|
204
|
-
logger.info('Processing bot config webhook', {
|
|
205
|
-
activityId: payload._id,
|
|
206
|
-
activityName: payload.name,
|
|
207
|
-
workspaceId,
|
|
208
|
-
phase: payload.currentPhase,
|
|
209
|
-
});
|
|
210
|
-
// Extract fields
|
|
211
|
-
const email = getFieldValue(payload.fields, 'agentEmailInHailer');
|
|
212
|
-
const password = getFieldValue(payload.fields, 'password');
|
|
213
|
-
const botType = getFieldValue(payload.fields, 'botType');
|
|
214
|
-
const userId = getFieldValue(payload.fields, 'hailerProfile');
|
|
215
|
-
const schemaConfigStr = getFieldValue(payload.fields, 'schemaConfig');
|
|
216
|
-
// Validate required fields
|
|
217
|
-
if (!email || !password) {
|
|
218
|
-
logger.warn('Webhook missing credentials', {
|
|
219
|
-
activityId: payload._id,
|
|
220
|
-
hasEmail: !!email,
|
|
221
|
-
hasPassword: !!password,
|
|
222
|
-
});
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
action: 'skip',
|
|
226
|
-
workspaceId,
|
|
227
|
-
botType,
|
|
228
|
-
error: 'Missing email or password',
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
// Parse schema config to determine if deployed or retired
|
|
232
|
-
let deployedPhaseId = null;
|
|
233
|
-
let retiredPhaseId = null;
|
|
234
|
-
if (schemaConfigStr) {
|
|
235
|
-
try {
|
|
236
|
-
const schemaConfig = JSON.parse(schemaConfigStr);
|
|
237
|
-
deployedPhaseId = schemaConfig.deployedPhaseId;
|
|
238
|
-
retiredPhaseId = schemaConfig.retiredPhaseId;
|
|
239
|
-
}
|
|
240
|
-
catch (e) {
|
|
241
|
-
logger.warn('Failed to parse schemaConfig', { schemaConfigStr });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
const isDeployed = deployedPhaseId ? payload.currentPhase === deployedPhaseId : true;
|
|
245
|
-
const isRetired = retiredPhaseId ? payload.currentPhase === retiredPhaseId : false;
|
|
246
|
-
const enabled = isDeployed && !isRetired;
|
|
247
|
-
// Load existing config
|
|
248
|
-
const config = loadWorkspaceConfig(workspaceId);
|
|
249
|
-
const botEntry = {
|
|
250
|
-
activityId: payload._id,
|
|
251
|
-
userId: userId || null,
|
|
252
|
-
email,
|
|
253
|
-
password,
|
|
254
|
-
botType: botType || 'unknown',
|
|
255
|
-
enabled,
|
|
256
|
-
displayName: payload.name, // Activity name from Agent Directory
|
|
257
|
-
};
|
|
258
|
-
let action;
|
|
259
|
-
// Handle orchestrator
|
|
260
|
-
if (botType === 'orchestrator') {
|
|
261
|
-
if (enabled) {
|
|
262
|
-
config.orchestrator = {
|
|
263
|
-
activityId: payload._id,
|
|
264
|
-
userId: userId || '',
|
|
265
|
-
email,
|
|
266
|
-
password,
|
|
267
|
-
displayName: payload.name,
|
|
268
|
-
};
|
|
269
|
-
action = 'update';
|
|
270
|
-
logger.info('Updated orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email), displayName: payload.name });
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
// Orchestrator disabled - remove it
|
|
274
|
-
if (config.orchestrator?.activityId === payload._id) {
|
|
275
|
-
delete config.orchestrator;
|
|
276
|
-
action = 'remove';
|
|
277
|
-
logger.info('Removed orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email) });
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
action = 'update';
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
// Handle specialist
|
|
286
|
-
const existingIndex = config.specialists.findIndex((s) => s.activityId === payload._id);
|
|
287
|
-
if (existingIndex >= 0) {
|
|
288
|
-
// Update existing
|
|
289
|
-
config.specialists[existingIndex] = botEntry;
|
|
290
|
-
action = enabled ? 'update' : 'remove';
|
|
291
|
-
}
|
|
292
|
-
else if (enabled) {
|
|
293
|
-
// Add new
|
|
294
|
-
config.specialists.push(botEntry);
|
|
295
|
-
action = 'add';
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
action = 'update';
|
|
299
|
-
}
|
|
300
|
-
logger.info('Updated specialist', {
|
|
301
|
-
workspaceId,
|
|
302
|
-
email,
|
|
303
|
-
botType,
|
|
304
|
-
enabled,
|
|
305
|
-
action,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
// Save config
|
|
309
|
-
saveWorkspaceConfig(config);
|
|
310
|
-
// Trigger callback for hot reload
|
|
311
|
-
if (botUpdateCallback) {
|
|
312
|
-
botUpdateCallback(workspaceId, botEntry, action);
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
success: true,
|
|
316
|
-
action,
|
|
317
|
-
workspaceId,
|
|
318
|
-
botType,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Get workspace config (for debugging/status)
|
|
323
|
-
*/
|
|
324
|
-
function getWorkspaceConfig(workspaceId) {
|
|
325
|
-
const config = loadWorkspaceConfig(workspaceId);
|
|
326
|
-
return config.orchestrator || config.specialists.length > 0 ? config : null;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* List all workspace configs
|
|
330
|
-
*/
|
|
331
|
-
function listWorkspaceConfigs() {
|
|
332
|
-
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
333
|
-
if (!fs.existsSync(configDir))
|
|
334
|
-
return [];
|
|
335
|
-
const files = fs.readdirSync(configDir).filter((f) => f.endsWith('.json'));
|
|
336
|
-
const configs = [];
|
|
337
|
-
for (const file of files) {
|
|
338
|
-
try {
|
|
339
|
-
const content = fs.readFileSync(path.join(configDir, file), 'utf-8');
|
|
340
|
-
configs.push(JSON.parse(content));
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
logger.warn('Failed to load workspace config', { file, error: String(error) });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return configs;
|
|
347
|
-
}
|
|
348
141
|
//# sourceMappingURL=webhook-handler.js.map
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -16,10 +16,6 @@ export interface MCPServerConfig {
|
|
|
16
16
|
port: number;
|
|
17
17
|
corsOrigins: string[];
|
|
18
18
|
toolRegistry: ToolRegistry;
|
|
19
|
-
getDaemonStatus?: () => Record<string, Array<{
|
|
20
|
-
botId: string;
|
|
21
|
-
state: any;
|
|
22
|
-
}>>;
|
|
23
19
|
}
|
|
24
20
|
export declare class MCPServerService {
|
|
25
21
|
private app;
|
package/dist/mcp-server.js
CHANGED
|
@@ -5,39 +5,6 @@
|
|
|
5
5
|
* Implements JSON-RPC 2.0 MCP protocol over HTTP with Server-Sent Events (SSE)
|
|
6
6
|
* for LLM clients (Claude Desktop, etc.)
|
|
7
7
|
*/
|
|
8
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
-
if (k2 === undefined) k2 = k;
|
|
10
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
-
}
|
|
14
|
-
Object.defineProperty(o, k2, desc);
|
|
15
|
-
}) : (function(o, m, k, k2) {
|
|
16
|
-
if (k2 === undefined) k2 = k;
|
|
17
|
-
o[k2] = m[k];
|
|
18
|
-
}));
|
|
19
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
-
}) : function(o, v) {
|
|
22
|
-
o["default"] = v;
|
|
23
|
-
});
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
8
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
10
|
};
|
|
@@ -49,13 +16,12 @@ const logger_1 = require("./lib/logger");
|
|
|
49
16
|
const config_1 = require("./config");
|
|
50
17
|
const UserContextCache_1 = require("./mcp/UserContextCache");
|
|
51
18
|
const tool_registry_1 = require("./mcp/tool-registry");
|
|
52
|
-
const webhook_handler_1 = require("./mcp/webhook-handler");
|
|
53
19
|
class MCPServerService {
|
|
54
20
|
app;
|
|
55
21
|
server;
|
|
56
22
|
logger;
|
|
57
23
|
config;
|
|
58
|
-
toolRegistry;
|
|
24
|
+
toolRegistry;
|
|
59
25
|
constructor(config) {
|
|
60
26
|
this.config = config;
|
|
61
27
|
this.toolRegistry = config.toolRegistry;
|
|
@@ -121,31 +87,15 @@ class MCPServerService {
|
|
|
121
87
|
status: 'ok',
|
|
122
88
|
timestamp: new Date().toISOString(),
|
|
123
89
|
service: 'hailer-mcp-server',
|
|
124
|
-
version:
|
|
90
|
+
version: '0.1.0'
|
|
125
91
|
};
|
|
126
92
|
res.json(health);
|
|
127
93
|
});
|
|
128
|
-
//
|
|
129
|
-
this.app.
|
|
130
|
-
|
|
131
|
-
res.status(404).json({ error: 'Daemon mode not enabled' });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const status = this.config.getDaemonStatus();
|
|
135
|
-
if (!status) {
|
|
136
|
-
res.status(503).json({ error: 'Daemon not running' });
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
res.json({
|
|
140
|
-
timestamp: new Date().toISOString(),
|
|
141
|
-
daemons: status
|
|
142
|
-
});
|
|
143
|
-
});
|
|
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) + '...' });
|
|
94
|
+
// MCP Protocol endpoint - JSON-RPC 2.0 over SSE
|
|
95
|
+
this.app.post('/api/mcp', async (req, res) => {
|
|
96
|
+
req.logger.debug('MCP request received', { method: req.body?.method });
|
|
148
97
|
try {
|
|
98
|
+
const apiKey = req.query.apiKey;
|
|
149
99
|
const mcpRequest = req.body;
|
|
150
100
|
let result;
|
|
151
101
|
if (mcpRequest.method === 'tools/list') {
|
|
@@ -168,23 +118,19 @@ class MCPServerService {
|
|
|
168
118
|
}
|
|
169
119
|
// Apply default tool group filtering
|
|
170
120
|
// - NUCLEAR: Only if ENABLE_NUCLEAR_TOOLS=true
|
|
171
|
-
// - BOT_INTERNAL: Only if explicitly requested via params.includeBotInternal (for daemons)
|
|
172
|
-
const includeBotInternal = mcpRequest.params?.includeBotInternal === true;
|
|
173
121
|
if (!filterConfig) {
|
|
174
|
-
// No filter yet - create default excluding NUCLEAR
|
|
122
|
+
// No filter yet - create default excluding NUCLEAR
|
|
175
123
|
filterConfig = {
|
|
176
124
|
allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND]
|
|
177
125
|
};
|
|
178
|
-
req.logger.debug('Using default tool filter (excludes NUCLEAR
|
|
126
|
+
req.logger.debug('Using default tool filter (excludes NUCLEAR)');
|
|
179
127
|
}
|
|
180
128
|
else if (filterConfig.allowedGroups) {
|
|
181
|
-
// Filter groups - remove
|
|
182
|
-
filterConfig.allowedGroups = filterConfig.allowedGroups.filter(g => (
|
|
183
|
-
(config_1.environment.ENABLE_NUCLEAR_TOOLS || g !== tool_registry_1.ToolGroup.NUCLEAR));
|
|
129
|
+
// Filter groups - remove NUCLEAR unless enabled
|
|
130
|
+
filterConfig.allowedGroups = filterConfig.allowedGroups.filter(g => (config_1.environment.ENABLE_NUCLEAR_TOOLS || g !== tool_registry_1.ToolGroup.NUCLEAR));
|
|
184
131
|
req.logger.debug('Filtered tool groups', {
|
|
185
132
|
allowedGroups: filterConfig.allowedGroups,
|
|
186
|
-
nuclearEnabled: config_1.environment.ENABLE_NUCLEAR_TOOLS
|
|
187
|
-
includeBotInternal
|
|
133
|
+
nuclearEnabled: config_1.environment.ENABLE_NUCLEAR_TOOLS
|
|
188
134
|
});
|
|
189
135
|
}
|
|
190
136
|
result = {
|
|
@@ -225,7 +171,7 @@ class MCPServerService {
|
|
|
225
171
|
capabilities: { tools: {} },
|
|
226
172
|
serverInfo: {
|
|
227
173
|
name: 'hailer-mcp-server',
|
|
228
|
-
version:
|
|
174
|
+
version: '1.0.0'
|
|
229
175
|
}
|
|
230
176
|
};
|
|
231
177
|
}
|
|
@@ -264,170 +210,13 @@ class MCPServerService {
|
|
|
264
210
|
this.sendMcpError(res, req.body?.id || null, -32000, `Server error: ${errorMessage}`, 500);
|
|
265
211
|
}
|
|
266
212
|
}
|
|
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
|
-
}
|
|
283
213
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const { AVAILABLE_BOTS, getBotState } = await Promise.resolve().then(() => __importStar(require('./bot-config')));
|
|
291
|
-
const state = getBotState();
|
|
292
|
-
const bots = AVAILABLE_BOTS.map(bot => ({
|
|
293
|
-
...bot,
|
|
294
|
-
enabled: state[bot.id] || false
|
|
295
|
-
}));
|
|
296
|
-
res.json({ bots });
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
req.logger.error('Failed to list bots', { error });
|
|
300
|
-
res.status(500).json({ error: 'Failed to list bots' });
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
// POST /api/bots/:id/enable - Enable a bot
|
|
304
|
-
this.app.post('/api/bots/:id/enable', async (req, res) => {
|
|
305
|
-
const { id } = req.params;
|
|
306
|
-
req.logger.info('Enable bot requested', { botId: id });
|
|
307
|
-
try {
|
|
308
|
-
const { AVAILABLE_BOTS, setBotEnabled } = await Promise.resolve().then(() => __importStar(require('./bot-config')));
|
|
309
|
-
const bot = AVAILABLE_BOTS.find(b => b.id === id);
|
|
310
|
-
if (!bot) {
|
|
311
|
-
return res.status(404).json({ error: `Unknown bot: ${id}` });
|
|
312
|
-
}
|
|
313
|
-
setBotEnabled(id, true);
|
|
314
|
-
res.json({ success: true, botId: id, enabled: true });
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
req.logger.error('Failed to enable bot', { botId: id, error });
|
|
318
|
-
res.status(500).json({ error: 'Failed to enable bot' });
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
// POST /api/bots/:id/disable - Disable a bot
|
|
322
|
-
this.app.post('/api/bots/:id/disable', async (req, res) => {
|
|
323
|
-
const { id } = req.params;
|
|
324
|
-
req.logger.info('Disable bot requested', { botId: id });
|
|
325
|
-
try {
|
|
326
|
-
const { AVAILABLE_BOTS, setBotEnabled } = await Promise.resolve().then(() => __importStar(require('./bot-config')));
|
|
327
|
-
const bot = AVAILABLE_BOTS.find(b => b.id === id);
|
|
328
|
-
if (!bot) {
|
|
329
|
-
return res.status(404).json({ error: `Unknown bot: ${id}` });
|
|
330
|
-
}
|
|
331
|
-
setBotEnabled(id, false);
|
|
332
|
-
res.json({ success: true, botId: id, enabled: false });
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
req.logger.error('Failed to disable bot', { botId: id, error });
|
|
336
|
-
res.status(500).json({ error: 'Failed to disable bot' });
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
// POST /api/bots/:id/toggle - Toggle a bot
|
|
340
|
-
this.app.post('/api/bots/:id/toggle', async (req, res) => {
|
|
341
|
-
const { id } = req.params;
|
|
342
|
-
req.logger.info('Toggle bot requested', { botId: id });
|
|
343
|
-
try {
|
|
344
|
-
const { AVAILABLE_BOTS, getBotState, setBotEnabled } = await Promise.resolve().then(() => __importStar(require('./bot-config')));
|
|
345
|
-
const bot = AVAILABLE_BOTS.find(b => b.id === id);
|
|
346
|
-
if (!bot) {
|
|
347
|
-
return res.status(404).json({ error: `Unknown bot: ${id}` });
|
|
348
|
-
}
|
|
349
|
-
const currentState = getBotState();
|
|
350
|
-
const newState = !currentState[id];
|
|
351
|
-
setBotEnabled(id, newState);
|
|
352
|
-
res.json({ success: true, botId: id, enabled: newState });
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
355
|
-
req.logger.error('Failed to toggle bot', { botId: id, error });
|
|
356
|
-
res.status(500).json({ error: 'Failed to toggle bot' });
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
// ===== Bot Config Webhook API =====
|
|
360
|
-
// Get secure webhook path (auto-generated token) - null if disabled
|
|
361
|
-
const webhookPath = (0, webhook_handler_1.getWebhookPath)();
|
|
362
|
-
if (webhookPath) {
|
|
363
|
-
// POST /webhook/{token} - Receives updates from Hailer workflow webhooks
|
|
364
|
-
this.app.post(webhookPath, (req, res) => {
|
|
365
|
-
req.logger.info('Bot config webhook received', {
|
|
366
|
-
activityId: req.body?._id,
|
|
367
|
-
activityName: req.body?.name,
|
|
368
|
-
workspaceId: req.body?.cid,
|
|
369
|
-
});
|
|
370
|
-
try {
|
|
371
|
-
const result = (0, webhook_handler_1.handleBotConfigWebhook)(req.body);
|
|
372
|
-
if (result.success) {
|
|
373
|
-
req.logger.info('Bot config updated via webhook', {
|
|
374
|
-
action: result.action,
|
|
375
|
-
workspaceId: result.workspaceId,
|
|
376
|
-
botType: result.botType,
|
|
377
|
-
});
|
|
378
|
-
res.status(200).json(result);
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
req.logger.warn('Bot config webhook failed', { error: result.error });
|
|
382
|
-
res.status(400).json(result);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
catch (error) {
|
|
386
|
-
req.logger.error('Bot config webhook error', { error });
|
|
387
|
-
res.status(500).json({
|
|
388
|
-
success: false,
|
|
389
|
-
error: error instanceof Error ? error.message : 'Internal error',
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
// GET /webhook/{token}/status - Status endpoint to see all workspace configs
|
|
394
|
-
this.app.get(`${webhookPath}/status`, (_req, res) => {
|
|
395
|
-
const configs = (0, webhook_handler_1.listWorkspaceConfigs)();
|
|
396
|
-
res.json({
|
|
397
|
-
timestamp: new Date().toISOString(),
|
|
398
|
-
workspaceCount: configs.length,
|
|
399
|
-
workspaces: configs.map((c) => ({
|
|
400
|
-
workspaceId: c.workspaceId,
|
|
401
|
-
workspaceName: c.workspaceName,
|
|
402
|
-
hasOrchestrator: !!c.orchestrator,
|
|
403
|
-
specialistCount: c.specialists.length,
|
|
404
|
-
enabledSpecialists: c.specialists.filter((s) => s.enabled).length,
|
|
405
|
-
lastSynced: c.lastSynced,
|
|
406
|
-
})),
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
this.logger.info('Webhook endpoint disabled (no WEBHOOK_TOKEN)');
|
|
412
|
-
}
|
|
413
|
-
this.logger.debug('Routes configured', {
|
|
414
|
-
routes: [
|
|
415
|
-
'/health',
|
|
416
|
-
'/daemon/status',
|
|
417
|
-
'/api/mcp',
|
|
418
|
-
'/api/bots'
|
|
419
|
-
]
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
this.logger.debug('Routes configured', {
|
|
424
|
-
routes: [
|
|
425
|
-
'/health',
|
|
426
|
-
'/daemon/status',
|
|
427
|
-
'/api/mcp'
|
|
428
|
-
]
|
|
429
|
-
});
|
|
430
|
-
}
|
|
214
|
+
this.logger.debug('Routes configured', {
|
|
215
|
+
routes: [
|
|
216
|
+
'/health',
|
|
217
|
+
'/api/mcp'
|
|
218
|
+
]
|
|
219
|
+
});
|
|
431
220
|
}
|
|
432
221
|
/**
|
|
433
222
|
* Check if agent has access to a specific tool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hailer/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"config": {
|
|
5
5
|
"docker": {
|
|
6
6
|
"registry": "registry.gitlab.com/hailer-repos/hailer-mcp"
|
|
@@ -10,22 +10,20 @@
|
|
|
10
10
|
"dev": "tsx watch src/app.ts",
|
|
11
11
|
"claude-code-mcp": "DISABLE_CLIENT=true tsx src/app.ts",
|
|
12
12
|
"start-mcp-only": "DISABLE_CLIENT=true tsx src/app.ts",
|
|
13
|
-
"start-client-only": "DISABLE_MCP_SERVER=true tsx src/app.ts",
|
|
14
13
|
"build": "tsc",
|
|
15
14
|
"start": "node dist/app.js",
|
|
16
15
|
"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
|
|
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
|
|
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
|
|
20
|
-
"build-k3d": "docker build -f Dockerfile.debug . -t $(npm pkg get config.docker.registry | xargs)/$(npm pkg get name | xargs
|
|
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
|
|
16
|
+
"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 .",
|
|
17
|
+
"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 .",
|
|
18
|
+
"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 .",
|
|
19
|
+
"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",
|
|
20
|
+
"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",
|
|
22
21
|
"mcp-server": "hailer-mcp",
|
|
23
|
-
"server": "tsx src/client/server.ts",
|
|
24
|
-
"server:prod": "node dist/client/server.js",
|
|
25
22
|
"release:patch": "npm version patch -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
26
23
|
"release:minor": "npm version minor -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
27
24
|
"release:major": "npm version major -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
|
|
28
|
-
"seed-config": "tsx src/commands/seed-config.ts"
|
|
25
|
+
"seed-config": "tsx src/commands/seed-config.ts",
|
|
26
|
+
"bot": "tsx src/bot/chat-bot.ts"
|
|
29
27
|
},
|
|
30
28
|
"dependencies": {
|
|
31
29
|
"@anthropic-ai/sdk": "^0.54.0",
|
|
@@ -37,6 +35,7 @@
|
|
|
37
35
|
"@opentelemetry/sdk-logs": "^0.57.0",
|
|
38
36
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
39
37
|
"@opentelemetry/semantic-conventions": "^1.36.0",
|
|
38
|
+
"axios": "^1.13.4",
|
|
40
39
|
"cors": "^2.8.5",
|
|
41
40
|
"dotenv": "^16.5.0",
|
|
42
41
|
"express": "^4.21.2",
|