@hailer/mcp 1.1.12 → 1.1.14
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/CHANGELOG.md +0 -7
- package/{.claude → dist}/CLAUDE.md +2 -2
- package/dist/app.js +18 -5
- package/dist/bot/bot-config.d.ts +10 -1
- package/dist/bot/bot-config.js +64 -3
- package/dist/bot/bot-manager.d.ts +2 -0
- package/dist/bot/bot-manager.js +9 -2
- package/dist/bot/bot.d.ts +33 -0
- package/dist/bot/bot.js +461 -160
- package/dist/bot/services/message-classifier.js +17 -0
- package/dist/bot/services/permission-guard.d.ts +52 -0
- package/dist/bot/services/permission-guard.js +149 -0
- package/dist/bot/services/types.d.ts +5 -0
- package/dist/bot/services/typing-indicator.d.ts +6 -1
- package/dist/bot/services/typing-indicator.js +19 -3
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +43 -0
- package/dist/core.js +3 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.d.ts +5 -0
- package/dist/mcp/UserContextCache.js +51 -19
- package/dist/mcp/hailer-clients.d.ts +19 -1
- package/dist/mcp/hailer-clients.js +158 -24
- package/dist/mcp/session-store.d.ts +68 -0
- package/dist/mcp/session-store.js +169 -0
- package/dist/mcp/signal-handler.js +2 -0
- package/dist/mcp/tool-registry.d.ts +17 -4
- package/dist/mcp/tool-registry.js +37 -7
- package/dist/mcp/tools/activity.js +99 -7
- package/dist/mcp/tools/app-scaffold.js +304 -336
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
- package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
- package/dist/mcp/tools/company.d.ts +9 -0
- package/dist/mcp/tools/company.js +88 -0
- package/dist/mcp/tools/discussion.js +68 -0
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
- package/dist/mcp/tools/workflow-permissions.js +204 -0
- package/dist/mcp/tools/workflow.js +57 -18
- package/dist/mcp/utils/index.d.ts +2 -0
- package/dist/mcp/utils/index.js +12 -1
- package/dist/mcp/utils/role-utils.d.ts +74 -0
- package/dist/mcp/utils/role-utils.js +151 -0
- package/dist/mcp/utils/types.d.ts +43 -1
- package/dist/mcp/utils/types.js +14 -0
- package/dist/mcp/webhook-handler.d.ts +4 -0
- package/dist/mcp/webhook-handler.js +8 -0
- package/dist/mcp-server.d.ts +23 -2
- package/dist/mcp-server.js +639 -127
- package/dist/plugins/vipunen/client.d.ts +150 -0
- package/dist/plugins/vipunen/client.js +535 -0
- package/dist/plugins/vipunen/config/schema-config.json +19 -0
- package/dist/plugins/vipunen/config/schema-doc.json +22 -0
- package/dist/plugins/vipunen/index.d.ts +41 -0
- package/dist/plugins/vipunen/index.js +88 -0
- package/dist/plugins/vipunen/tools.d.ts +26 -0
- package/dist/plugins/vipunen/tools.js +501 -0
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +2 -1
- package/.claude/agents/agent-ada-skill-builder.md +0 -94
- package/.claude/agents/agent-alejandro-function-fields.md +0 -342
- package/.claude/agents/agent-bjorn-config-audit.md +0 -103
- package/.claude/agents/agent-builder-agent-creator.md +0 -130
- package/.claude/agents/agent-code-simplifier.md +0 -53
- package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
- package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
- package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
- package/.claude/agents/agent-helga-workflow-config.md +0 -204
- package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
- package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
- package/.claude/agents/agent-ivan-monolith.md +0 -154
- package/.claude/agents/agent-kenji-data-reader.md +0 -86
- package/.claude/agents/agent-lars-code-inspector.md +0 -102
- package/.claude/agents/agent-marco-mockup-builder.md +0 -110
- package/.claude/agents/agent-marcus-api-documenter.md +0 -323
- package/.claude/agents/agent-marketplace-publisher.md +0 -280
- package/.claude/agents/agent-marketplace-reviewer.md +0 -309
- package/.claude/agents/agent-permissions-handler.md +0 -208
- package/.claude/agents/agent-simple-writer.md +0 -48
- package/.claude/agents/agent-svetlana-code-review.md +0 -171
- package/.claude/agents/agent-tanya-test-runner.md +0 -333
- package/.claude/agents/agent-ui-designer.md +0 -100
- package/.claude/agents/agent-viktor-sql-insights.md +0 -212
- package/.claude/agents/agent-web-search.md +0 -55
- package/.claude/agents/agent-yevgeni-discussions.md +0 -45
- package/.claude/agents/agent-zara-zapier.md +0 -159
- package/.claude/commands/app-squad.md +0 -135
- package/.claude/commands/audit-squad.md +0 -158
- package/.claude/commands/autoplan.md +0 -563
- package/.claude/commands/cleanup-squad.md +0 -98
- package/.claude/commands/config-squad.md +0 -106
- package/.claude/commands/crud-squad.md +0 -87
- package/.claude/commands/data-squad.md +0 -97
- package/.claude/commands/debug-squad.md +0 -303
- package/.claude/commands/doc-squad.md +0 -65
- package/.claude/commands/handoff.md +0 -137
- package/.claude/commands/health.md +0 -49
- package/.claude/commands/help.md +0 -29
- package/.claude/commands/help:agents.md +0 -151
- package/.claude/commands/help:commands.md +0 -78
- package/.claude/commands/help:faq.md +0 -79
- package/.claude/commands/help:plugins.md +0 -50
- package/.claude/commands/help:skills.md +0 -93
- package/.claude/commands/help:tools.md +0 -75
- package/.claude/commands/hotfix-squad.md +0 -112
- package/.claude/commands/integration-squad.md +0 -82
- package/.claude/commands/janitor-squad.md +0 -167
- package/.claude/commands/learn-auto.md +0 -120
- package/.claude/commands/learn.md +0 -120
- package/.claude/commands/mcp-list.md +0 -27
- package/.claude/commands/onboard-squad.md +0 -140
- package/.claude/commands/plan-workspace.md +0 -732
- package/.claude/commands/prd.md +0 -130
- package/.claude/commands/project-status.md +0 -82
- package/.claude/commands/publish.md +0 -138
- package/.claude/commands/recap.md +0 -69
- package/.claude/commands/restore.md +0 -64
- package/.claude/commands/review-squad.md +0 -152
- package/.claude/commands/save.md +0 -24
- package/.claude/commands/stats.md +0 -19
- package/.claude/commands/swarm.md +0 -210
- package/.claude/commands/tool-builder.md +0 -39
- package/.claude/commands/ws-pull.md +0 -44
- package/.claude/hooks/_shared-memory.cjs +0 -305
- package/.claude/hooks/_utils.cjs +0 -108
- package/.claude/hooks/agent-failure-detector.cjs +0 -383
- package/.claude/hooks/agent-usage-logger.cjs +0 -204
- package/.claude/hooks/app-edit-guard.cjs +0 -494
- package/.claude/hooks/auto-learn.cjs +0 -304
- package/.claude/hooks/bash-guard.cjs +0 -272
- package/.claude/hooks/builder-mode-manager.cjs +0 -354
- package/.claude/hooks/bulk-activity-guard.cjs +0 -271
- package/.claude/hooks/context-watchdog.cjs +0 -230
- package/.claude/hooks/delegation-reminder.cjs +0 -465
- package/.claude/hooks/design-system-lint.cjs +0 -271
- package/.claude/hooks/post-scaffold-hook.cjs +0 -181
- package/.claude/hooks/prompt-guard.cjs +0 -354
- package/.claude/hooks/publish-template-guard.cjs +0 -147
- package/.claude/hooks/session-start.cjs +0 -35
- package/.claude/hooks/shared-memory-writer.cjs +0 -147
- package/.claude/hooks/skill-injector.cjs +0 -140
- package/.claude/hooks/skill-usage-logger.cjs +0 -258
- package/.claude/hooks/src-edit-guard.cjs +0 -240
- package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
- package/.claude/settings.json +0 -257
- package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
- package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
- package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
- package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
- package/.claude/skills/agent-structure/SKILL.md +0 -98
- package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
- package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
- package/.claude/skills/delegation-routing/SKILL.md +0 -202
- package/.claude/skills/frontend-design/SKILL.md +0 -254
- package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
- package/.claude/skills/hailer-api-client/SKILL.md +0 -518
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
- package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
- package/.claude/skills/hailer-design-system/SKILL.md +0 -235
- package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
- package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
- package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
- package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
- package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
- package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
- package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
- package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
- package/.claude/skills/integration-patterns/SKILL.md +0 -421
- package/.claude/skills/json-only-output/SKILL.md +0 -72
- package/.claude/skills/lsp-setup/SKILL.md +0 -160
- package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
- package/.claude/skills/optional-parameters/SKILL.md +0 -72
- package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
- package/.claude/skills/testing-patterns/SKILL.md +0 -630
- package/.claude/skills/tool-builder/SKILL.md +0 -250
- package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
- package/.claude/skills/tool-response-verification/SKILL.md +0 -92
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
- package/.mcp.json +0 -13
- package/.opencode/agent/agent-ada-skill-builder.md +0 -35
- package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
- package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
- package/.opencode/agent/agent-builder-agent-creator.md +0 -39
- package/.opencode/agent/agent-code-simplifier.md +0 -31
- package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
- package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
- package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
- package/.opencode/agent/agent-helga-workflow-config.md +0 -203
- package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
- package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
- package/.opencode/agent/agent-ivan-monolith.md +0 -46
- package/.opencode/agent/agent-kenji-data-reader.md +0 -53
- package/.opencode/agent/agent-lars-code-inspector.md +0 -28
- package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
- package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
- package/.opencode/agent/agent-marketplace-publisher.md +0 -44
- package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
- package/.opencode/agent/agent-permissions-handler.md +0 -50
- package/.opencode/agent/agent-simple-writer.md +0 -45
- package/.opencode/agent/agent-svetlana-code-review.md +0 -39
- package/.opencode/agent/agent-tanya-test-runner.md +0 -57
- package/.opencode/agent/agent-ui-designer.md +0 -56
- package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
- package/.opencode/agent/agent-web-search.md +0 -42
- package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
- package/.opencode/agent/agent-zara-zapier.md +0 -53
- package/.opencode/commands/app-squad.md +0 -135
- package/.opencode/commands/audit-squad.md +0 -158
- package/.opencode/commands/autoplan.md +0 -563
- package/.opencode/commands/cleanup-squad.md +0 -98
- package/.opencode/commands/config-squad.md +0 -106
- package/.opencode/commands/crud-squad.md +0 -87
- package/.opencode/commands/data-squad.md +0 -97
- package/.opencode/commands/debug-squad.md +0 -303
- package/.opencode/commands/doc-squad.md +0 -65
- package/.opencode/commands/handoff.md +0 -137
- package/.opencode/commands/health.md +0 -49
- package/.opencode/commands/help-agents.md +0 -151
- package/.opencode/commands/help-commands.md +0 -32
- package/.opencode/commands/help-faq.md +0 -29
- package/.opencode/commands/help-plugins.md +0 -28
- package/.opencode/commands/help-skills.md +0 -7
- package/.opencode/commands/help-tools.md +0 -40
- package/.opencode/commands/help.md +0 -28
- package/.opencode/commands/hotfix-squad.md +0 -112
- package/.opencode/commands/integration-squad.md +0 -82
- package/.opencode/commands/janitor-squad.md +0 -167
- package/.opencode/commands/learn-auto.md +0 -120
- package/.opencode/commands/learn.md +0 -120
- package/.opencode/commands/mcp-list.md +0 -27
- package/.opencode/commands/onboard-squad.md +0 -140
- package/.opencode/commands/plan-workspace.md +0 -732
- package/.opencode/commands/prd.md +0 -131
- package/.opencode/commands/project-status.md +0 -82
- package/.opencode/commands/publish.md +0 -138
- package/.opencode/commands/recap.md +0 -69
- package/.opencode/commands/restore.md +0 -64
- package/.opencode/commands/review-squad.md +0 -152
- package/.opencode/commands/save.md +0 -24
- package/.opencode/commands/stats.md +0 -19
- package/.opencode/commands/swarm.md +0 -210
- package/.opencode/commands/tool-builder.md +0 -39
- package/.opencode/commands/ws-pull.md +0 -44
- package/.opencode/opencode.json +0 -28
- package/SESSION-HANDOFF.md +0 -68
- package/inbox/2026-03-04-bot-config-patterns.md +0 -24
package/dist/mcp-server.js
CHANGED
|
@@ -45,20 +45,48 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.MCPServerService = void 0;
|
|
46
46
|
const express_1 = __importDefault(require("express"));
|
|
47
47
|
const cors_1 = __importDefault(require("cors"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
48
50
|
const logger_1 = require("./lib/logger");
|
|
49
51
|
const config_1 = require("./config");
|
|
50
52
|
const UserContextCache_1 = require("./mcp/UserContextCache");
|
|
51
53
|
const tool_registry_1 = require("./mcp/tool-registry");
|
|
54
|
+
const session_store_1 = require("./mcp/session-store");
|
|
52
55
|
const webhook_handler_1 = require("./mcp/webhook-handler");
|
|
56
|
+
const vipunen_1 = require("./plugins/vipunen");
|
|
57
|
+
// Load MCP instructions for Claude App
|
|
58
|
+
// Try dist/mcp/ first (production), then src/mcp/ (development)
|
|
59
|
+
let mcpInstructions;
|
|
60
|
+
const instructionsPaths = [
|
|
61
|
+
path.join(__dirname, 'mcp', 'instructions.md'), // dist/mcp/instructions.md
|
|
62
|
+
path.join(__dirname, '..', 'src', 'mcp', 'instructions.md'), // src/mcp/instructions.md (from dist/)
|
|
63
|
+
path.join(process.cwd(), 'src', 'mcp', 'instructions.md') // src/mcp/instructions.md (from cwd)
|
|
64
|
+
];
|
|
65
|
+
for (const instructionsPath of instructionsPaths) {
|
|
66
|
+
try {
|
|
67
|
+
mcpInstructions = fs.readFileSync(instructionsPath, 'utf-8');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Try next path
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
class MCPServerService {
|
|
75
|
+
static ENDPOINTS = {
|
|
76
|
+
hailer: '/api/mcp',
|
|
77
|
+
cowork: '/api/cowork/mcp',
|
|
78
|
+
vipunen: '/api/vipunen'
|
|
79
|
+
};
|
|
54
80
|
app;
|
|
55
81
|
server;
|
|
56
82
|
logger;
|
|
57
83
|
config;
|
|
58
|
-
toolRegistry;
|
|
84
|
+
toolRegistry;
|
|
85
|
+
appConfig;
|
|
59
86
|
constructor(config) {
|
|
60
87
|
this.config = config;
|
|
61
88
|
this.toolRegistry = config.toolRegistry;
|
|
89
|
+
this.appConfig = (0, config_1.createApplicationConfig)();
|
|
62
90
|
this.logger = (0, logger_1.createLogger)({
|
|
63
91
|
component: 'mcp-server',
|
|
64
92
|
service: 'hailer-mcp-server'
|
|
@@ -66,7 +94,7 @@ class MCPServerService {
|
|
|
66
94
|
this.app = (0, express_1.default)();
|
|
67
95
|
this.setupMiddleware();
|
|
68
96
|
this.setupRoutes();
|
|
69
|
-
this.logger.
|
|
97
|
+
this.logger.info('MCP Server initialized', {
|
|
70
98
|
port: config.port,
|
|
71
99
|
corsOrigins: config.corsOrigins
|
|
72
100
|
});
|
|
@@ -114,19 +142,278 @@ class MCPServerService {
|
|
|
114
142
|
next();
|
|
115
143
|
});
|
|
116
144
|
}
|
|
145
|
+
escapeHtml(s) {
|
|
146
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
147
|
+
}
|
|
148
|
+
getBaseUrl(req) {
|
|
149
|
+
const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'https';
|
|
150
|
+
const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost:3030';
|
|
151
|
+
return `${protocol}://${host}`;
|
|
152
|
+
}
|
|
117
153
|
setupRoutes() {
|
|
118
|
-
const appConfig = (0, config_1.createApplicationConfig)();
|
|
119
154
|
// Health check endpoint (no logging - too noisy)
|
|
120
155
|
this.app.get('/health', (_, res) => {
|
|
121
156
|
const health = {
|
|
122
157
|
status: 'ok',
|
|
123
158
|
timestamp: new Date().toISOString(),
|
|
124
159
|
service: 'hailer-mcp-server',
|
|
125
|
-
version: config_1.APP_VERSION
|
|
160
|
+
version: config_1.APP_VERSION,
|
|
161
|
+
endpoints: MCPServerService.ENDPOINTS
|
|
126
162
|
};
|
|
127
163
|
res.json(health);
|
|
128
164
|
});
|
|
129
|
-
//
|
|
165
|
+
// API prefix for Claude App / cowork endpoints
|
|
166
|
+
const API_PREFIX = '/api/cowork';
|
|
167
|
+
// OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
|
168
|
+
// This tells mcp-remote where to send users for authorization
|
|
169
|
+
this.app.get('/.well-known/oauth-authorization-server', (req, res) => {
|
|
170
|
+
const baseUrl = this.getBaseUrl(req);
|
|
171
|
+
res.json({
|
|
172
|
+
issuer: baseUrl,
|
|
173
|
+
authorization_endpoint: `${baseUrl}${API_PREFIX}/oauth/authorize`,
|
|
174
|
+
token_endpoint: `${baseUrl}${API_PREFIX}/oauth/token`,
|
|
175
|
+
registration_endpoint: `${baseUrl}${API_PREFIX}/oauth/register`,
|
|
176
|
+
response_types_supported: ['code'],
|
|
177
|
+
grant_types_supported: ['authorization_code'],
|
|
178
|
+
code_challenge_methods_supported: ['S256'],
|
|
179
|
+
token_endpoint_auth_methods_supported: ['none']
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
// Dynamic Client Registration (RFC 7591)
|
|
183
|
+
// mcp-remote calls this before starting OAuth flow
|
|
184
|
+
this.app.post(`${API_PREFIX}/oauth/register`, (req, res) => {
|
|
185
|
+
const { client_name, redirect_uris } = req.body;
|
|
186
|
+
req.logger.info('OAuth client registration', { client_name, redirect_uris });
|
|
187
|
+
// Generate a client_id for this registration
|
|
188
|
+
const clientId = `mcp_client_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
189
|
+
// Return client credentials (RFC 7591 response)
|
|
190
|
+
res.status(201).json({
|
|
191
|
+
client_id: clientId,
|
|
192
|
+
client_name: client_name || 'MCP Client',
|
|
193
|
+
redirect_uris: redirect_uris || [],
|
|
194
|
+
grant_types: ['authorization_code'],
|
|
195
|
+
response_types: ['code'],
|
|
196
|
+
token_endpoint_auth_method: 'none'
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// OAuth Protected Resource Metadata (for MCP endpoint)
|
|
200
|
+
// Note: mcp-remote requests both /.well-known/oauth-protected-resource AND
|
|
201
|
+
// /.well-known/oauth-protected-resource{API_PREFIX}/mcp - we handle both
|
|
202
|
+
const protectedResourceHandler = (req, res) => {
|
|
203
|
+
const baseUrl = this.getBaseUrl(req);
|
|
204
|
+
res.json({
|
|
205
|
+
resource: `${baseUrl}${API_PREFIX}/mcp`,
|
|
206
|
+
authorization_servers: [baseUrl]
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
this.app.get('/.well-known/oauth-protected-resource', protectedResourceHandler);
|
|
210
|
+
this.app.get(`/.well-known/oauth-protected-resource${API_PREFIX}/mcp`, protectedResourceHandler);
|
|
211
|
+
// OAuth Authorize endpoint - redirects to Hailer login
|
|
212
|
+
// Supports both Claude Desktop native connector (redirect_uri=claude.ai) and mcp-remote (localhost)
|
|
213
|
+
this.app.get(`${API_PREFIX}/oauth/authorize`, (req, res) => {
|
|
214
|
+
const { redirect_uri, state, code_challenge, code_challenge_method, client_id } = req.query;
|
|
215
|
+
req.logger.info('OAuth authorize request', {
|
|
216
|
+
redirect_uri,
|
|
217
|
+
client_id,
|
|
218
|
+
state: state ? 'present' : 'missing',
|
|
219
|
+
code_challenge: code_challenge ? 'present' : 'missing'
|
|
220
|
+
});
|
|
221
|
+
// Store PKCE challenge AND original redirect_uri for token exchange
|
|
222
|
+
if (state) {
|
|
223
|
+
const pkceKey = `pkce_${state}`;
|
|
224
|
+
session_store_1.sessionStore.set(pkceKey, code_challenge || '', redirect_uri || '');
|
|
225
|
+
}
|
|
226
|
+
// Determine the callback URL for Hailer
|
|
227
|
+
const baseUrl = this.getBaseUrl(req);
|
|
228
|
+
const ourCallback = `${baseUrl}${API_PREFIX}/oauth/callback`;
|
|
229
|
+
// Redirect to Hailer authorization page (hash routing)
|
|
230
|
+
// The Hailer frontend will create API key and redirect back with code+state
|
|
231
|
+
const params = new URLSearchParams();
|
|
232
|
+
params.set('redirectUri', ourCallback);
|
|
233
|
+
params.set('authorizeAppName', 'Claude');
|
|
234
|
+
params.set('state', state || '');
|
|
235
|
+
// Use HAILER_APP_URL env var, default to local dev
|
|
236
|
+
const hailerAppUrl = process.env.HAILER_APP_URL || 'https://app.hailer.com';
|
|
237
|
+
const hailerAuthUrl = `${hailerAppUrl}/#/authorize/user-api-key?${params.toString()}`;
|
|
238
|
+
res.redirect(hailerAuthUrl);
|
|
239
|
+
});
|
|
240
|
+
// OAuth Token endpoint - exchanges code for access token
|
|
241
|
+
// In our flow, the "code" IS the key_id (already registered by frontend)
|
|
242
|
+
this.app.post(`${API_PREFIX}/oauth/token`, express_1.default.urlencoded({ extended: true }), (req, res) => {
|
|
243
|
+
const { grant_type, code, code_verifier, redirect_uri } = req.body;
|
|
244
|
+
req.logger.info('OAuth token request', { grant_type, hasCode: !!code });
|
|
245
|
+
if (grant_type !== 'authorization_code') {
|
|
246
|
+
return res.status(400).json({ error: 'unsupported_grant_type' });
|
|
247
|
+
}
|
|
248
|
+
if (!code) {
|
|
249
|
+
return res.status(400).json({ error: 'invalid_request', error_description: 'code is required' });
|
|
250
|
+
}
|
|
251
|
+
// The "code" from Hailer frontend is actually the key_id
|
|
252
|
+
// It was already registered via /auth/register by the frontend
|
|
253
|
+
const session = session_store_1.sessionStore.get(code);
|
|
254
|
+
if (!session) {
|
|
255
|
+
req.logger.warn('OAuth token exchange failed - session not found', { code: code.substring(0, 8) + '...' });
|
|
256
|
+
return res.status(400).json({ error: 'invalid_grant', error_description: 'Invalid or expired code' });
|
|
257
|
+
}
|
|
258
|
+
// Return the key_id as access_token (real apiKey stays in session store)
|
|
259
|
+
req.logger.info('OAuth token issued', { keyId: code.substring(0, 8) + '...' });
|
|
260
|
+
res.json({
|
|
261
|
+
access_token: code,
|
|
262
|
+
token_type: 'Bearer',
|
|
263
|
+
expires_in: 86400 // 24 hours (matches session TTL)
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
// OAuth Callback endpoint - receives code+state from Hailer frontend via GET redirect
|
|
267
|
+
// Then redirects to the original redirect_uri (Claude Desktop or mcp-remote)
|
|
268
|
+
this.app.get(`${API_PREFIX}/oauth/callback`, (req, res) => {
|
|
269
|
+
const { code, state, error } = req.query;
|
|
270
|
+
req.logger.info('OAuth callback GET', {
|
|
271
|
+
hasCode: !!code,
|
|
272
|
+
hasState: !!state,
|
|
273
|
+
error
|
|
274
|
+
});
|
|
275
|
+
if (error) {
|
|
276
|
+
req.logger.warn('OAuth callback received error', { error });
|
|
277
|
+
return res.status(400).send(`
|
|
278
|
+
<html><body>
|
|
279
|
+
<h1>Authorization Failed</h1>
|
|
280
|
+
<p>Error: ${this.escapeHtml(String(error))}</p>
|
|
281
|
+
<p>You can close this window.</p>
|
|
282
|
+
</body></html>
|
|
283
|
+
`);
|
|
284
|
+
}
|
|
285
|
+
if (!code || !state) {
|
|
286
|
+
req.logger.warn('OAuth callback missing code or state');
|
|
287
|
+
return res.status(400).send(`
|
|
288
|
+
<html><body>
|
|
289
|
+
<h1>Authorization Failed</h1>
|
|
290
|
+
<p>Missing code or state parameter</p>
|
|
291
|
+
</body></html>
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
// Look up the original redirect_uri from PKCE storage
|
|
295
|
+
const pkceKey = `pkce_${state}`;
|
|
296
|
+
const pkceSession = session_store_1.sessionStore.get(pkceKey);
|
|
297
|
+
if (!pkceSession) {
|
|
298
|
+
req.logger.warn('OAuth callback - PKCE session not found', { state });
|
|
299
|
+
return res.status(400).send(`
|
|
300
|
+
<html><body>
|
|
301
|
+
<h1>Authorization Failed</h1>
|
|
302
|
+
<p>Session expired or invalid state</p>
|
|
303
|
+
</body></html>
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
const originalRedirectUri = pkceSession.workspaceId; // We stored redirect_uri in workspaceId field
|
|
307
|
+
req.logger.info('OAuth callback redirecting', {
|
|
308
|
+
keyId: code.substring(0, 8) + '...',
|
|
309
|
+
redirectUri: originalRedirectUri
|
|
310
|
+
});
|
|
311
|
+
// Redirect to original redirect_uri with code and state
|
|
312
|
+
let finalUrl;
|
|
313
|
+
try {
|
|
314
|
+
finalUrl = new URL(originalRedirectUri);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
req.logger.error('OAuth callback - invalid redirect_uri', { originalRedirectUri });
|
|
318
|
+
return res.status(400).send('<html><body><h1>Authorization Failed</h1><p>Invalid redirect URI.</p></body></html>');
|
|
319
|
+
}
|
|
320
|
+
finalUrl.searchParams.set('code', code);
|
|
321
|
+
finalUrl.searchParams.set('state', state);
|
|
322
|
+
res.redirect(finalUrl.toString());
|
|
323
|
+
});
|
|
324
|
+
// Legacy POST callback for backwards compatibility
|
|
325
|
+
this.app.post(`${API_PREFIX}/oauth/callback`, express_1.default.urlencoded({ extended: true }), (req, res) => {
|
|
326
|
+
req.logger.debug('OAuth callback POST body', { body: req.body });
|
|
327
|
+
const { access_token, error } = req.body;
|
|
328
|
+
if (error) {
|
|
329
|
+
return res.status(400).send(`<html><body><h1>Authorization Failed</h1><p>Error: ${this.escapeHtml(String(error))}</p></body></html>`);
|
|
330
|
+
}
|
|
331
|
+
if (!access_token) {
|
|
332
|
+
return res.status(400).send(`<html><body><h1>Authorization Failed</h1><p>Missing access token</p></body></html>`);
|
|
333
|
+
}
|
|
334
|
+
req.logger.info('OAuth callback POST received', { keyId: access_token.substring(0, 8) + '...' });
|
|
335
|
+
res.send(`
|
|
336
|
+
<html><body>
|
|
337
|
+
<h1>Authorization Successful!</h1>
|
|
338
|
+
<p>You can close this window and return to Claude.</p>
|
|
339
|
+
</body></html>
|
|
340
|
+
`);
|
|
341
|
+
});
|
|
342
|
+
// Auth register endpoint - receives key_id + api_key from Hailer frontend
|
|
343
|
+
// Called after user login to register the session mapping
|
|
344
|
+
this.app.post(`${API_PREFIX}/auth/register`, (req, res) => {
|
|
345
|
+
const { key_id, api_key, workspace_id } = req.body;
|
|
346
|
+
if (!key_id || !api_key) {
|
|
347
|
+
req.logger.warn('Auth register missing required fields', { hasKeyId: !!key_id, hasApiKey: !!api_key });
|
|
348
|
+
return res.status(400).json({ error: 'key_id and api_key are required' });
|
|
349
|
+
}
|
|
350
|
+
session_store_1.sessionStore.set(key_id, api_key, workspace_id || '');
|
|
351
|
+
req.logger.info('Auth session registered', { keyId: key_id.substring(0, 8) + '...' });
|
|
352
|
+
res.json({ status: 'ok' });
|
|
353
|
+
});
|
|
354
|
+
// Session stats endpoint (for debugging)
|
|
355
|
+
this.app.get(`${API_PREFIX}/auth/stats`, (req, res) => {
|
|
356
|
+
const adminToken = req.headers['x-admin-token'];
|
|
357
|
+
if (!adminToken || adminToken !== process.env.WEBHOOK_TOKEN) {
|
|
358
|
+
return res.status(403).json({ error: 'forbidden' });
|
|
359
|
+
}
|
|
360
|
+
res.json(session_store_1.sessionStore.getStats());
|
|
361
|
+
});
|
|
362
|
+
// ===== Bot Config Webhook API =====
|
|
363
|
+
// Get secure webhook path (auto-generated token) - null if disabled
|
|
364
|
+
const webhookPath = (0, webhook_handler_1.getWebhookPath)();
|
|
365
|
+
if (webhookPath) {
|
|
366
|
+
// POST /webhook/{token} - Receives updates from Hailer workflow webhooks
|
|
367
|
+
this.app.post(webhookPath, (req, res) => {
|
|
368
|
+
req.logger.debug('Bot config webhook received', {
|
|
369
|
+
activityId: req.body?._id,
|
|
370
|
+
activityName: req.body?.name,
|
|
371
|
+
workspaceId: req.body?.cid,
|
|
372
|
+
});
|
|
373
|
+
try {
|
|
374
|
+
const result = (0, webhook_handler_1.handleBotConfigWebhook)(req.body);
|
|
375
|
+
if (result.success) {
|
|
376
|
+
req.logger.debug('Bot config updated via webhook', {
|
|
377
|
+
action: result.action,
|
|
378
|
+
workspaceId: result.workspaceId,
|
|
379
|
+
botType: result.botType,
|
|
380
|
+
});
|
|
381
|
+
res.status(200).json(result);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
req.logger.warn('Bot config webhook failed', { error: result.error });
|
|
385
|
+
res.status(400).json(result);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
req.logger.error('Bot config webhook error', { error });
|
|
390
|
+
res.status(500).json({
|
|
391
|
+
success: false,
|
|
392
|
+
error: error instanceof Error ? error.message : 'Internal error',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
// GET /webhook/{token}/status - Status endpoint to see all workspace configs
|
|
397
|
+
this.app.get(`${webhookPath}/status`, (_req, res) => {
|
|
398
|
+
const configs = (0, webhook_handler_1.listWorkspaceConfigs)();
|
|
399
|
+
res.json({
|
|
400
|
+
timestamp: new Date().toISOString(),
|
|
401
|
+
workspaceCount: configs.length,
|
|
402
|
+
workspaces: configs.map((c) => ({
|
|
403
|
+
workspaceId: c.workspaceId,
|
|
404
|
+
workspaceName: c.workspaceName,
|
|
405
|
+
hasOrchestrator: !!c.orchestrator,
|
|
406
|
+
specialistCount: c.specialists.length,
|
|
407
|
+
enabledSpecialists: c.specialists.filter((s) => s.enabled).length,
|
|
408
|
+
lastSynced: c.lastSynced,
|
|
409
|
+
})),
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
this.logger.debug('Webhook endpoint disabled (no WEBHOOK_TOKEN)');
|
|
415
|
+
}
|
|
416
|
+
// ===== Daemon status endpoint =====
|
|
130
417
|
this.app.get('/daemon/status', (_, res) => {
|
|
131
418
|
if (!this.config.getDaemonStatus) {
|
|
132
419
|
res.status(404).json({ error: 'Daemon mode not enabled' });
|
|
@@ -137,25 +424,25 @@ class MCPServerService {
|
|
|
137
424
|
res.status(503).json({ error: 'Daemon not running' });
|
|
138
425
|
return;
|
|
139
426
|
}
|
|
140
|
-
res.json({
|
|
141
|
-
timestamp: new Date().toISOString(),
|
|
142
|
-
daemons: status
|
|
143
|
-
});
|
|
427
|
+
res.json({ timestamp: new Date().toISOString(), daemons: status });
|
|
144
428
|
});
|
|
145
|
-
// MCP
|
|
429
|
+
// ===== Hailer MCP endpoint — Direct API key auth (for Claude Code, mcp-remote, SDK) =====
|
|
430
|
+
// Restored original mcpHandler from pre-Cowork era with BOT_INTERNAL filtering and strict access control
|
|
146
431
|
const mcpHandler = async (req, res, apiKeyOverride) => {
|
|
147
432
|
const apiKey = apiKeyOverride || req.query.apiKey;
|
|
148
433
|
req.logger.debug('MCP request received', { method: req.body?.method, apiKey: apiKey?.slice(0, 8) + '...' });
|
|
149
434
|
try {
|
|
150
435
|
const mcpRequest = req.body;
|
|
436
|
+
if (!mcpRequest.params) {
|
|
437
|
+
mcpRequest.params = {};
|
|
438
|
+
}
|
|
151
439
|
let result;
|
|
152
440
|
if (mcpRequest.method === 'tools/list') {
|
|
153
441
|
req.logger.debug('Handling tools/list request');
|
|
154
|
-
// Get agent configuration for filtering
|
|
155
442
|
let filterConfig;
|
|
156
443
|
if (apiKey) {
|
|
157
444
|
try {
|
|
158
|
-
const agentConfig = appConfig.getClientConfig(apiKey);
|
|
445
|
+
const agentConfig = this.appConfig.getClientConfig(apiKey);
|
|
159
446
|
if (agentConfig.allowedTools || agentConfig.allowedGroups) {
|
|
160
447
|
filterConfig = {
|
|
161
448
|
allowedTools: agentConfig.allowedTools,
|
|
@@ -167,26 +454,17 @@ class MCPServerService {
|
|
|
167
454
|
req.logger.debug('No config found for apiKey, returning all tools');
|
|
168
455
|
}
|
|
169
456
|
}
|
|
170
|
-
//
|
|
171
|
-
// - NUCLEAR: Only if ENABLE_NUCLEAR_TOOLS=true
|
|
172
|
-
// - BOT_INTERNAL: Only if explicitly requested via params.includeBotInternal (for daemons)
|
|
457
|
+
// BOT_INTERNAL filtering: only include if explicitly requested (for daemons)
|
|
173
458
|
const includeBotInternal = mcpRequest.params?.includeBotInternal === true;
|
|
174
459
|
if (!filterConfig) {
|
|
175
|
-
// No filter yet - create default excluding NUCLEAR and BOT_INTERNAL
|
|
176
460
|
filterConfig = {
|
|
177
461
|
allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND]
|
|
178
462
|
};
|
|
179
463
|
req.logger.debug('Using default tool filter (excludes NUCLEAR and BOT_INTERNAL)');
|
|
180
464
|
}
|
|
181
465
|
else if (filterConfig.allowedGroups) {
|
|
182
|
-
// Filter groups - remove BOT_INTERNAL unless explicitly requested, remove NUCLEAR unless enabled
|
|
183
466
|
filterConfig.allowedGroups = filterConfig.allowedGroups.filter(g => (includeBotInternal || g !== tool_registry_1.ToolGroup.BOT_INTERNAL) &&
|
|
184
467
|
(config_1.environment.ENABLE_NUCLEAR_TOOLS || g !== tool_registry_1.ToolGroup.NUCLEAR));
|
|
185
|
-
req.logger.debug('Filtered tool groups', {
|
|
186
|
-
allowedGroups: filterConfig.allowedGroups,
|
|
187
|
-
nuclearEnabled: config_1.environment.ENABLE_NUCLEAR_TOOLS,
|
|
188
|
-
includeBotInternal
|
|
189
|
-
});
|
|
190
468
|
}
|
|
191
469
|
result = {
|
|
192
470
|
tools: this.toolRegistry.getToolDefinitions(filterConfig)
|
|
@@ -208,10 +486,13 @@ class MCPServerService {
|
|
|
208
486
|
if (!apiKey) {
|
|
209
487
|
return this.sendMcpError(res, mcpRequest.id, -32602, 'API key required for tools/call', 400);
|
|
210
488
|
}
|
|
489
|
+
if (!mcpRequest.params?.name) {
|
|
490
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, 'params.name is required', 400);
|
|
491
|
+
}
|
|
211
492
|
const { name, arguments: args = {} } = mcpRequest.params;
|
|
212
493
|
req.logger.info('Tool call', { tool: name });
|
|
213
|
-
//
|
|
214
|
-
if (!this.
|
|
494
|
+
// Strict access control — returns false on catch (no config = no access)
|
|
495
|
+
if (!this.canAccessToolStrict(name, apiKey)) {
|
|
215
496
|
return this.sendMcpError(res, mcpRequest.id, -32603, `Access denied to tool: ${name}`, 403);
|
|
216
497
|
}
|
|
217
498
|
const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
|
|
@@ -253,12 +534,9 @@ class MCPServerService {
|
|
|
253
534
|
}
|
|
254
535
|
catch (error) {
|
|
255
536
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
256
|
-
// Check for multi-workspace credentials error - return user-friendly error
|
|
257
|
-
// Use HTTP 200 so the JSON-RPC error reaches the client properly
|
|
258
537
|
if (errorMessage.includes('Multi-workspace credentials detected')) {
|
|
259
538
|
req.logger.warn('Multi-workspace credentials blocked', { apiKey: req.query.apiKey });
|
|
260
|
-
this.sendMcpError(res, req.body?.id || null, -32001, errorMessage, 200
|
|
261
|
-
);
|
|
539
|
+
this.sendMcpError(res, req.body?.id || null, -32001, errorMessage, 200);
|
|
262
540
|
}
|
|
263
541
|
else {
|
|
264
542
|
req.logger.error('MCP handler error', error, { apiKey: req.query.apiKey });
|
|
@@ -266,25 +544,20 @@ class MCPServerService {
|
|
|
266
544
|
}
|
|
267
545
|
}
|
|
268
546
|
};
|
|
269
|
-
// MCP Protocol endpoint - JSON-RPC 2.0 over SSE
|
|
270
547
|
// Route 1: /api/mcp?apiKey=xxx (standard format)
|
|
271
548
|
this.app.post('/api/mcp', (req, res) => mcpHandler(req, res));
|
|
272
549
|
// Route 2: /:apiKey (simplified format - API key as path)
|
|
273
550
|
// Matches 16-64 char alphanumeric keys, but ONLY for MCP requests (has jsonrpc field)
|
|
274
|
-
// Non-MCP requests (webhooks) pass through to later routes
|
|
275
551
|
this.app.post('/:apiKey([a-zA-Z0-9_-]{16,64})', (req, res, next) => {
|
|
276
552
|
if (req.body?.jsonrpc) {
|
|
277
|
-
// MCP request - handle it
|
|
278
553
|
mcpHandler(req, res, req.params.apiKey);
|
|
279
554
|
}
|
|
280
555
|
else {
|
|
281
|
-
// Not MCP (likely webhook) - pass to next route
|
|
282
556
|
next();
|
|
283
557
|
}
|
|
284
558
|
});
|
|
285
559
|
// ===== Bot Configuration API (only when MCP_CLIENT_ENABLED=true) =====
|
|
286
560
|
if (config_1.environment.MCP_CLIENT_ENABLED) {
|
|
287
|
-
// GET /api/bots - List all bots and their status
|
|
288
561
|
this.app.get('/api/bots', async (req, res) => {
|
|
289
562
|
req.logger.debug('List bots requested');
|
|
290
563
|
try {
|
|
@@ -301,7 +574,6 @@ class MCPServerService {
|
|
|
301
574
|
res.status(500).json({ error: 'Failed to list bots' });
|
|
302
575
|
}
|
|
303
576
|
});
|
|
304
|
-
// POST /api/bots/:id/enable - Enable a bot
|
|
305
577
|
this.app.post('/api/bots/:id/enable', async (req, res) => {
|
|
306
578
|
const { id } = req.params;
|
|
307
579
|
req.logger.debug('Enable bot requested', { botId: id });
|
|
@@ -319,7 +591,6 @@ class MCPServerService {
|
|
|
319
591
|
res.status(500).json({ error: 'Failed to enable bot' });
|
|
320
592
|
}
|
|
321
593
|
});
|
|
322
|
-
// POST /api/bots/:id/disable - Disable a bot
|
|
323
594
|
this.app.post('/api/bots/:id/disable', async (req, res) => {
|
|
324
595
|
const { id } = req.params;
|
|
325
596
|
req.logger.debug('Disable bot requested', { botId: id });
|
|
@@ -337,7 +608,6 @@ class MCPServerService {
|
|
|
337
608
|
res.status(500).json({ error: 'Failed to disable bot' });
|
|
338
609
|
}
|
|
339
610
|
});
|
|
340
|
-
// POST /api/bots/:id/toggle - Toggle a bot
|
|
341
611
|
this.app.post('/api/bots/:id/toggle', async (req, res) => {
|
|
342
612
|
const { id } = req.params;
|
|
343
613
|
req.logger.debug('Toggle bot requested', { botId: id });
|
|
@@ -357,101 +627,358 @@ class MCPServerService {
|
|
|
357
627
|
res.status(500).json({ error: 'Failed to toggle bot' });
|
|
358
628
|
}
|
|
359
629
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
630
|
+
}
|
|
631
|
+
// ===== Cowork MCP endpoint — OAuth multi-user (for Claude.ai native connector) =====
|
|
632
|
+
// MCP session termination endpoint (required by MCP spec)
|
|
633
|
+
// NOTE: This only closes the MCP transport, NOT the OAuth session
|
|
634
|
+
// OAuth sessions have their own TTL (24h) and should persist across MCP reconnects
|
|
635
|
+
this.app.delete(`${API_PREFIX}/mcp`, (req, res) => {
|
|
636
|
+
req.logger.debug('Cowork MCP session close request');
|
|
637
|
+
// Don't delete OAuth session - it should persist for reconnects
|
|
638
|
+
res.status(200).json({ success: true });
|
|
639
|
+
});
|
|
640
|
+
// ===== Vipunen endpoint — API key auth only, no OAuth, no session store =====
|
|
641
|
+
// DELETE /api/vipunen - session close
|
|
642
|
+
this.app.delete('/api/vipunen', (req, res) => {
|
|
643
|
+
req.logger.debug('Vipunen MCP session close request');
|
|
644
|
+
res.status(200).json({ success: true });
|
|
645
|
+
});
|
|
646
|
+
// GET /api/vipunen - SSE stream
|
|
647
|
+
this.app.get('/api/vipunen', (req, res) => {
|
|
648
|
+
req.logger.debug('Vipunen SSE connection opened');
|
|
649
|
+
this.setupSseStream(req, res, 'Vipunen ');
|
|
650
|
+
});
|
|
651
|
+
// POST /api/vipunen - JSON-RPC 2.0, Vipunen tools only
|
|
652
|
+
this.app.post('/api/vipunen', async (req, res) => {
|
|
653
|
+
req.logger.debug('Vipunen MCP request received', { method: req.body?.method });
|
|
654
|
+
try {
|
|
655
|
+
const mcpRequest = req.body;
|
|
656
|
+
if (mcpRequest.method === 'initialize') {
|
|
657
|
+
req.logger.info('Vipunen MCP initialize request received');
|
|
658
|
+
res.setHeader('Mcp-Session-Id', this.generateSessionId('vipunen-session'));
|
|
659
|
+
return this.sendMcpResult(res, mcpRequest.id, {
|
|
660
|
+
protocolVersion: '2024-11-05',
|
|
661
|
+
capabilities: { tools: {} },
|
|
662
|
+
serverInfo: {
|
|
663
|
+
name: 'vipunen-server',
|
|
664
|
+
version: '1.0.0'
|
|
384
665
|
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
else if (mcpRequest.method === 'notifications/initialized') {
|
|
669
|
+
req.logger.info('Vipunen MCP handshake completed');
|
|
670
|
+
res.status(204).end();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
else if (mcpRequest.method === 'tools/list') {
|
|
674
|
+
const vipunenDefs = this.toolRegistry.getToolDefinitionsByContextType('none');
|
|
675
|
+
req.logger.debug('Vipunen tools/list', { count: vipunenDefs.length });
|
|
676
|
+
return this.sendMcpResult(res, mcpRequest.id, { tools: vipunenDefs });
|
|
677
|
+
}
|
|
678
|
+
else if (mcpRequest.method === 'tools/call') {
|
|
679
|
+
if (!mcpRequest.params?.name) {
|
|
680
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, 'params.name is required', 400);
|
|
385
681
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
682
|
+
const bearerToken = this.extractBearerToken(req);
|
|
683
|
+
// Require valid Vipunen API key
|
|
684
|
+
if (!bearerToken || !(0, vipunen_1.isValidVipunenKey)(bearerToken)) {
|
|
685
|
+
req.logger.warn('Vipunen tools/call - invalid or missing API key');
|
|
686
|
+
res.setHeader('WWW-Authenticate', 'Bearer');
|
|
687
|
+
return res.status(401).json({
|
|
688
|
+
error: 'unauthorized',
|
|
689
|
+
error_description: 'Valid Vipunen API key required.'
|
|
391
690
|
});
|
|
392
691
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
692
|
+
const { name, arguments: args = {} } = mcpRequest.params;
|
|
693
|
+
req.logger.debug('Vipunen tools/call', { toolName: name });
|
|
694
|
+
const tool = this.toolRegistry.getTool(name);
|
|
695
|
+
if (!tool) {
|
|
696
|
+
return this.sendMcpError(res, mcpRequest.id, -32601, `Unknown tool: ${name}`, 404);
|
|
697
|
+
}
|
|
698
|
+
if (tool.contextType !== 'none') {
|
|
699
|
+
return this.sendMcpError(res, mcpRequest.id, -32603, `Tool not accessible via Vipunen endpoint: ${name}`, 403);
|
|
700
|
+
}
|
|
701
|
+
const vipunenCtx = (0, vipunen_1.resolveVipunenContext)(bearerToken);
|
|
702
|
+
const result = await this.toolRegistry.executeTool(name, args, vipunenCtx);
|
|
703
|
+
return this.sendMcpResult(res, mcpRequest.id, result);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
return this.sendMcpError(res, mcpRequest.id, -32601, `Method not found: ${mcpRequest.method}`, 400);
|
|
707
|
+
}
|
|
410
708
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
709
|
+
catch (error) {
|
|
710
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
711
|
+
req.logger.error('Vipunen MCP handler error', error);
|
|
712
|
+
this.sendMcpError(res, req.body?.id || null, -32000, `Server error: ${errorMessage}`, 500);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
// Cowork MCP Protocol endpoint - GET for SSE stream
|
|
716
|
+
this.app.get(`${API_PREFIX}/mcp`, (req, res) => {
|
|
717
|
+
req.logger.debug('Cowork SSE connection opened');
|
|
718
|
+
this.setupSseStream(req, res, 'Cowork ');
|
|
719
|
+
});
|
|
720
|
+
// Cowork MCP Protocol endpoint - JSON-RPC 2.0 over SSE (OAuth auth)
|
|
721
|
+
this.app.post(`${API_PREFIX}/mcp`, (req, res) => {
|
|
722
|
+
this.handleCoworkMcp(req, res, {
|
|
723
|
+
resolveApiKey: (r) => {
|
|
724
|
+
const bearer = this.extractBearerToken(r);
|
|
725
|
+
let apiKey;
|
|
726
|
+
let keyId;
|
|
727
|
+
if (bearer) {
|
|
728
|
+
keyId = bearer;
|
|
729
|
+
const session = session_store_1.sessionStore.get(keyId);
|
|
730
|
+
if (session) {
|
|
731
|
+
apiKey = session.apiKey;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Fallback to query param (legacy support)
|
|
735
|
+
if (!apiKey) {
|
|
736
|
+
apiKey = r.query.apiKey;
|
|
737
|
+
}
|
|
738
|
+
return { apiKey, keyId };
|
|
739
|
+
},
|
|
740
|
+
onUnauthorized: (r, rs, _id, toolName) => {
|
|
741
|
+
const baseUrl = this.getBaseUrl(r);
|
|
742
|
+
r.logger.info('No auth for tools/call - returning 401 to trigger OAuth', { tool: toolName });
|
|
743
|
+
rs.setHeader('WWW-Authenticate', `Bearer resource="${baseUrl}${API_PREFIX}/mcp"`);
|
|
744
|
+
rs.status(401).json({
|
|
745
|
+
error: 'unauthorized',
|
|
746
|
+
error_description: 'Authentication required. Please authorize via OAuth to use tools.'
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
label: 'Cowork'
|
|
421
750
|
});
|
|
751
|
+
});
|
|
752
|
+
this.logger.debug('Routes configured', {
|
|
753
|
+
routes: [
|
|
754
|
+
'/health',
|
|
755
|
+
'/daemon/status',
|
|
756
|
+
'/api/mcp (Hailer — direct API key)',
|
|
757
|
+
'/:apiKey (Hailer — API key as path)',
|
|
758
|
+
'/api/bots (Bot management)',
|
|
759
|
+
`${API_PREFIX}/mcp (Cowork — OAuth)`,
|
|
760
|
+
'/api/vipunen (Vipunen — Bearer key)',
|
|
761
|
+
'/.well-known/oauth-authorization-server',
|
|
762
|
+
'/.well-known/oauth-protected-resource',
|
|
763
|
+
`${API_PREFIX}/oauth/register`,
|
|
764
|
+
`${API_PREFIX}/oauth/authorize`,
|
|
765
|
+
`${API_PREFIX}/oauth/token`,
|
|
766
|
+
`${API_PREFIX}/oauth/callback`,
|
|
767
|
+
`${API_PREFIX}/auth/register`,
|
|
768
|
+
`${API_PREFIX}/auth/stats`
|
|
769
|
+
]
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
extractBearerToken(req) {
|
|
773
|
+
const auth = req.headers.authorization;
|
|
774
|
+
return auth?.startsWith('Bearer ') ? auth.substring(7) : undefined;
|
|
775
|
+
}
|
|
776
|
+
setupSseStream(req, res, label) {
|
|
777
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
778
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
779
|
+
res.setHeader('Connection', 'keep-alive');
|
|
780
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
781
|
+
res.write(': connected\n\n');
|
|
782
|
+
const heartbeat = setInterval(() => {
|
|
783
|
+
if (!res.write(': heartbeat\n\n')) {
|
|
784
|
+
clearInterval(heartbeat);
|
|
785
|
+
}
|
|
786
|
+
}, 30000);
|
|
787
|
+
req.on('close', () => {
|
|
788
|
+
clearInterval(heartbeat);
|
|
789
|
+
req.logger.debug(`${label}SSE connection closed`);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
generateSessionId(prefix) {
|
|
793
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Strict access control for /api/mcp — returns false on catch (no config = no access)
|
|
797
|
+
*/
|
|
798
|
+
canAccessToolStrict(toolName, apiKey) {
|
|
799
|
+
// User API Keys aren't in CLIENT_CONFIGS — apply permissive access (non-NUCLEAR)
|
|
800
|
+
if (apiKey.startsWith('userapikey_')) {
|
|
801
|
+
const tool = this.toolRegistry.getTool(toolName);
|
|
802
|
+
if (!tool)
|
|
803
|
+
return false;
|
|
804
|
+
if (tool.group === tool_registry_1.ToolGroup.NUCLEAR && !config_1.environment.ENABLE_NUCLEAR_TOOLS) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
return true;
|
|
422
808
|
}
|
|
423
|
-
|
|
424
|
-
this.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
809
|
+
try {
|
|
810
|
+
const agentConfig = this.appConfig.getClientConfig(apiKey);
|
|
811
|
+
if (agentConfig.allowedTools) {
|
|
812
|
+
return agentConfig.allowedTools.includes(toolName);
|
|
813
|
+
}
|
|
814
|
+
if (agentConfig.allowedGroups) {
|
|
815
|
+
const tool = this.toolRegistry.getTool(toolName);
|
|
816
|
+
return tool ? agentConfig.allowedGroups.includes(tool.group) : false;
|
|
817
|
+
}
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
return false;
|
|
431
822
|
}
|
|
432
823
|
}
|
|
433
824
|
/**
|
|
434
|
-
*
|
|
825
|
+
* Permissive access control for Cowork — allows non-NUCLEAR tools on catch (OAuth sessions)
|
|
435
826
|
*/
|
|
436
|
-
|
|
827
|
+
canAccessToolPermissive(toolName, apiKey) {
|
|
437
828
|
try {
|
|
438
|
-
const agentConfig = appConfig.getClientConfig(apiKey);
|
|
439
|
-
// Explicit tool whitelist (takes precedence)
|
|
829
|
+
const agentConfig = this.appConfig.getClientConfig(apiKey);
|
|
440
830
|
if (agentConfig.allowedTools) {
|
|
441
831
|
return agentConfig.allowedTools.includes(toolName);
|
|
442
832
|
}
|
|
443
|
-
// Group-based access control
|
|
444
833
|
if (agentConfig.allowedGroups) {
|
|
445
834
|
const tool = this.toolRegistry.getTool(toolName);
|
|
446
835
|
return tool ? agentConfig.allowedGroups.includes(tool.group) : false;
|
|
447
836
|
}
|
|
448
|
-
// Default: allow all (backward compatible)
|
|
449
837
|
return true;
|
|
450
838
|
}
|
|
451
839
|
catch {
|
|
452
|
-
|
|
840
|
+
const tool = this.toolRegistry.getTool(toolName);
|
|
841
|
+
if (!tool)
|
|
842
|
+
return false;
|
|
843
|
+
if (tool.group === tool_registry_1.ToolGroup.NUCLEAR && !config_1.environment.ENABLE_NUCLEAR_TOOLS) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
453
847
|
}
|
|
454
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* Cowork MCP JSON-RPC handler for /api/cowork/mcp (OAuth multi-user).
|
|
851
|
+
* Permissive access control, contextType filter, OAuth 401 flow.
|
|
852
|
+
*/
|
|
853
|
+
async handleCoworkMcp(req, res, options) {
|
|
854
|
+
req.logger.debug(`${options.label} MCP request received`, { method: req.body?.method });
|
|
855
|
+
try {
|
|
856
|
+
const { apiKey, keyId } = options.resolveApiKey(req);
|
|
857
|
+
if (apiKey) {
|
|
858
|
+
req.logger.debug(`${options.label} API key resolved`, { keyId: keyId?.substring(0, 8) || 'direct' });
|
|
859
|
+
}
|
|
860
|
+
const mcpRequest = req.body;
|
|
861
|
+
let result;
|
|
862
|
+
// tools/list is intentionally unauthenticated — MCP clients need the list
|
|
863
|
+
// to construct the OAuth prompt before they have a token.
|
|
864
|
+
// tools/call is the enforcement gate.
|
|
865
|
+
if (mcpRequest.method === 'tools/list') {
|
|
866
|
+
req.logger.debug(`${options.label} Handling tools/list`);
|
|
867
|
+
let filterConfig;
|
|
868
|
+
if (apiKey) {
|
|
869
|
+
try {
|
|
870
|
+
const agentConfig = this.appConfig.getClientConfig(apiKey);
|
|
871
|
+
if (agentConfig.allowedTools || agentConfig.allowedGroups) {
|
|
872
|
+
filterConfig = {
|
|
873
|
+
allowedTools: agentConfig.allowedTools,
|
|
874
|
+
allowedGroups: agentConfig.allowedGroups
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
req.logger.debug('No config found for apiKey, returning all tools');
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (!filterConfig) {
|
|
883
|
+
filterConfig = {
|
|
884
|
+
allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND]
|
|
885
|
+
};
|
|
886
|
+
req.logger.debug('Using default tool filter (excludes NUCLEAR)');
|
|
887
|
+
}
|
|
888
|
+
else if (filterConfig.allowedGroups) {
|
|
889
|
+
filterConfig.allowedGroups = filterConfig.allowedGroups.filter(g => (config_1.environment.ENABLE_NUCLEAR_TOOLS || g !== tool_registry_1.ToolGroup.NUCLEAR));
|
|
890
|
+
}
|
|
891
|
+
result = {
|
|
892
|
+
tools: this.toolRegistry.getToolDefinitions(filterConfig)
|
|
893
|
+
.filter((def) => {
|
|
894
|
+
const tool = this.toolRegistry.getTool(def.name);
|
|
895
|
+
return (tool?.contextType ?? 'hailer') !== 'none';
|
|
896
|
+
})
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
else if (mcpRequest.method === 'tools/get_schema') {
|
|
900
|
+
const toolName = mcpRequest.params?.name;
|
|
901
|
+
req.logger.debug(`${options.label} Handling tools/get_schema`, { toolName });
|
|
902
|
+
if (!toolName) {
|
|
903
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, 'Tool name required in params.name', 400);
|
|
904
|
+
}
|
|
905
|
+
const schema = this.toolRegistry.getToolSchema(toolName);
|
|
906
|
+
if (!schema) {
|
|
907
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, `Tool not found: ${toolName}`, 404);
|
|
908
|
+
}
|
|
909
|
+
result = schema;
|
|
910
|
+
}
|
|
911
|
+
else if (mcpRequest.method === 'tools/call') {
|
|
912
|
+
if (!apiKey) {
|
|
913
|
+
return options.onUnauthorized(req, res, mcpRequest.id, mcpRequest.params?.name);
|
|
914
|
+
}
|
|
915
|
+
if (!mcpRequest.params?.name) {
|
|
916
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, 'params.name is required', 400);
|
|
917
|
+
}
|
|
918
|
+
const { name, arguments: args = {} } = mcpRequest.params;
|
|
919
|
+
req.logger.debug(`${options.label} Handling tools/call`, { toolName: name });
|
|
920
|
+
const tool = this.toolRegistry.getTool(name);
|
|
921
|
+
const contextType = tool?.contextType ?? 'hailer';
|
|
922
|
+
if (!this.canAccessToolPermissive(name, apiKey)) {
|
|
923
|
+
return this.sendMcpError(res, mcpRequest.id, -32603, `Access denied to tool: ${name}`, 403);
|
|
924
|
+
}
|
|
925
|
+
if (contextType === 'none') {
|
|
926
|
+
return this.sendMcpError(res, mcpRequest.id, -32602, `Tool not found: ${name}`, 404);
|
|
927
|
+
}
|
|
928
|
+
const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
|
|
929
|
+
result = await this.toolRegistry.executeTool(name, args, userContext);
|
|
930
|
+
}
|
|
931
|
+
else if (mcpRequest.method === 'initialize') {
|
|
932
|
+
req.logger.info(`${options.label} MCP initialize request received`);
|
|
933
|
+
res.setHeader('Mcp-Session-Id', this.generateSessionId('session'));
|
|
934
|
+
result = {
|
|
935
|
+
protocolVersion: '2024-11-05',
|
|
936
|
+
capabilities: { tools: {} },
|
|
937
|
+
serverInfo: {
|
|
938
|
+
name: 'hailer-mcp-server',
|
|
939
|
+
version: '1.0.0'
|
|
940
|
+
},
|
|
941
|
+
// Instructions help Claude App understand how to use Hailer tools
|
|
942
|
+
...(mcpInstructions && { instructions: mcpInstructions })
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
else if (mcpRequest.method === 'notifications/initialized') {
|
|
946
|
+
req.logger.info(`${options.label} MCP handshake completed`);
|
|
947
|
+
res.status(204).end();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
return this.sendMcpError(res, mcpRequest.id, -32601, `Method not found: ${mcpRequest.method}`, 400);
|
|
952
|
+
}
|
|
953
|
+
this.sendMcpResult(res, mcpRequest.id, result);
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
957
|
+
if (errorMessage.includes('Multi-workspace credentials detected')) {
|
|
958
|
+
req.logger.warn('Multi-workspace credentials blocked');
|
|
959
|
+
this.sendMcpError(res, req.body?.id || null, -32001, errorMessage, 200);
|
|
960
|
+
}
|
|
961
|
+
else if (errorMessage.includes('No Hailer account found')) {
|
|
962
|
+
req.logger.warn('Invalid API key - no Hailer account found');
|
|
963
|
+
this.sendMcpError(res, req.body?.id || null, -32002, 'Invalid or expired API key. Please disconnect and reconnect the Hailer MCP connector to re-authorize.', 200);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
req.logger.error(`${options.label} MCP handler error`, error);
|
|
967
|
+
this.sendMcpError(res, req.body?.id || null, -32000, `Server error: ${errorMessage}`, 500);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Send MCP success response via SSE
|
|
973
|
+
*/
|
|
974
|
+
sendMcpResult(res, id, result) {
|
|
975
|
+
const response = { jsonrpc: '2.0', result, id };
|
|
976
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
977
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
978
|
+
res.setHeader('Connection', 'keep-alive');
|
|
979
|
+
res.write(`data: ${JSON.stringify(response)}\n\n`);
|
|
980
|
+
res.end();
|
|
981
|
+
}
|
|
455
982
|
/**
|
|
456
983
|
* Send MCP error response
|
|
457
984
|
*/
|
|
@@ -472,11 +999,12 @@ class MCPServerService {
|
|
|
472
999
|
this.logger.info('MCP Server started', {
|
|
473
1000
|
port: port,
|
|
474
1001
|
healthCheck: `http://localhost:${port}/health`,
|
|
475
|
-
|
|
1002
|
+
daemonStatus: `http://localhost:${port}/daemon/status`,
|
|
1003
|
+
hailerEndpoint: `http://localhost:${port}/api/mcp`,
|
|
1004
|
+
coworkEndpoint: `http://localhost:${port}/api/cowork/mcp`,
|
|
1005
|
+
vipunenEndpoint: `http://localhost:${port}/api/vipunen`
|
|
476
1006
|
});
|
|
477
1007
|
// Update .mcp.json with the actual port so Claude Code connects to the right server
|
|
478
|
-
const fs = require('fs');
|
|
479
|
-
const path = require('path');
|
|
480
1008
|
const mcpJsonPath = path.join(process.cwd(), '.mcp.json');
|
|
481
1009
|
try {
|
|
482
1010
|
if (fs.existsSync(mcpJsonPath)) {
|
|
@@ -495,22 +1023,6 @@ class MCPServerService {
|
|
|
495
1023
|
catch (err) {
|
|
496
1024
|
this.logger.debug('Could not update .mcp.json', { error: err.message });
|
|
497
1025
|
}
|
|
498
|
-
// Update .opencode/opencode.json with the actual port so OpenCode connects to the right server
|
|
499
|
-
const opencodeJsonPath = path.join(process.cwd(), '.opencode', 'opencode.json');
|
|
500
|
-
try {
|
|
501
|
-
if (fs.existsSync(opencodeJsonPath)) {
|
|
502
|
-
const opencodeJson = JSON.parse(fs.readFileSync(opencodeJsonPath, 'utf-8'));
|
|
503
|
-
const hailerMcp = opencodeJson?.mcp?.hailer;
|
|
504
|
-
if (hailerMcp?.url && typeof hailerMcp.url === 'string' && hailerMcp.url.includes('localhost:')) {
|
|
505
|
-
hailerMcp.url = hailerMcp.url.replace(/localhost:\d+/, `localhost:${port}`);
|
|
506
|
-
fs.writeFileSync(opencodeJsonPath, JSON.stringify(opencodeJson, null, 2) + '\n');
|
|
507
|
-
this.logger.debug('.opencode/opencode.json updated with port', { port });
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
this.logger.debug('Could not update .opencode/opencode.json', { error: err.message });
|
|
513
|
-
}
|
|
514
1026
|
resolve();
|
|
515
1027
|
});
|
|
516
1028
|
this.server.on('error', (error) => {
|
|
@@ -524,7 +1036,7 @@ class MCPServerService {
|
|
|
524
1036
|
const server = this.server;
|
|
525
1037
|
return new Promise((resolve) => {
|
|
526
1038
|
server.close(() => {
|
|
527
|
-
this.logger.
|
|
1039
|
+
this.logger.info('MCP Server stopped gracefully');
|
|
528
1040
|
resolve();
|
|
529
1041
|
});
|
|
530
1042
|
});
|