@hailer/mcp 1.1.10 → 1.1.11

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Investigation Tool - Spawn Claude Code agents to investigate local repos
3
+ *
4
+ * Uses `claude -p` (headless mode, subscription-based) to autonomously
5
+ * explore codebases, search for bugs, and analyze code.
6
+ */
7
+ import { Tool } from '../tool-registry';
8
+ export declare const investigateRepoTool: Tool;
9
+ //# sourceMappingURL=investigate.d.ts.map
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ /**
3
+ * Investigation Tool - Spawn Claude Code agents to investigate local repos
4
+ *
5
+ * Uses `claude -p` (headless mode, subscription-based) to autonomously
6
+ * explore codebases, search for bugs, and analyze code.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.investigateRepoTool = void 0;
10
+ const zod_1 = require("zod");
11
+ const child_process_1 = require("child_process");
12
+ const tool_registry_1 = require("../tool-registry");
13
+ const logger_1 = require("../../lib/logger");
14
+ const config_1 = require("../../config");
15
+ const logger = (0, logger_1.createLogger)({ component: 'investigate-tool' });
16
+ /**
17
+ * Resolve claude CLI path at module load
18
+ */
19
+ const CLAUDE_BIN = (() => {
20
+ try {
21
+ return (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
22
+ }
23
+ catch {
24
+ return 'claude'; // fallback to PATH lookup
25
+ }
26
+ })();
27
+ /**
28
+ * Parse DEV_AI_REPOS env var into a name->path map.
29
+ * Format: "name1:/path/one,name2:/path/two"
30
+ */
31
+ function parseRepoPaths() {
32
+ const raw = config_1.environment.DEV_AI_REPOS;
33
+ const map = new Map();
34
+ if (!raw)
35
+ return map;
36
+ for (const entry of raw.split(',')) {
37
+ const colonIdx = entry.indexOf(':');
38
+ if (colonIdx === -1)
39
+ continue;
40
+ const name = entry.slice(0, colonIdx).trim();
41
+ const repoPath = entry.slice(colonIdx + 1).trim();
42
+ if (name && repoPath) {
43
+ map.set(name, repoPath);
44
+ }
45
+ }
46
+ return map;
47
+ }
48
+ /**
49
+ * Resolve a repo name to its local filesystem path.
50
+ */
51
+ function resolveRepoPath(repoName) {
52
+ const repos = parseRepoPaths();
53
+ return repos.get(repoName) || null;
54
+ }
55
+ /**
56
+ * Spawn `claude -p` in a repo directory and return its output.
57
+ */
58
+ async function spawnInvestigation(prompt, cwd, maxTurns, repoName) {
59
+ const timeout = config_1.environment.DEV_AI_INVESTIGATION_TIMEOUT;
60
+ return new Promise((resolve, reject) => {
61
+ const proc = (0, child_process_1.spawn)(CLAUDE_BIN, [
62
+ '-p', prompt,
63
+ '--output-format', 'stream-json',
64
+ '--verbose',
65
+ '--allowedTools', 'Read,Glob,Grep,Bash,Task',
66
+ '--max-turns', String(maxTurns),
67
+ ], {
68
+ cwd,
69
+ timeout,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ env: {
72
+ ...process.env,
73
+ // Strip MCP vars so investigation agent doesn't connect to our MCP server
74
+ MCP_SERVER_URL: undefined,
75
+ MCP_CLIENT_API_KEY: undefined,
76
+ MCP_CLIENT_ENABLED: undefined,
77
+ CLIENT_CONFIGS: undefined,
78
+ BOT_EMAIL: undefined,
79
+ BOT_PASSWORD: undefined,
80
+ BOT_API_BASE_URL: undefined,
81
+ ANTHROPIC_API_KEY: undefined,
82
+ },
83
+ });
84
+ let lineBuffer = '';
85
+ let resultText = '';
86
+ let stderr = '';
87
+ proc.stdout.on('data', (data) => {
88
+ lineBuffer += data.toString();
89
+ const lines = lineBuffer.split('\n');
90
+ lineBuffer = lines.pop() || ''; // Keep incomplete last line in buffer
91
+ for (const line of lines) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed)
94
+ continue;
95
+ try {
96
+ const event = JSON.parse(trimmed);
97
+ if (event.type === 'system' && event.subtype === 'init') {
98
+ logger.debug(`[investigation:${repoName}] Agent initialized | tools: ${(event.tools || []).length} | model: ${event.model || 'unknown'}`);
99
+ }
100
+ else if (event.type === 'assistant' && event.message?.content) {
101
+ for (const block of event.message.content) {
102
+ if (block.type === 'tool_use') {
103
+ const inputStr = JSON.stringify(block.input || {});
104
+ logger.debug(`[investigation:${repoName}] Tool: ${block.name} ${inputStr.length > 200 ? inputStr.slice(0, 200) + '...' : inputStr}`);
105
+ }
106
+ else if (block.type === 'text') {
107
+ const text = (block.text || '').trim();
108
+ if (text) {
109
+ logger.debug(`[investigation:${repoName}] ${text.length > 300 ? text.slice(0, 300) + '...' : text}`);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ else if (event.type === 'tool') {
115
+ const content = typeof event.content === 'string' ? event.content : JSON.stringify(event.content || '');
116
+ logger.debug(`[investigation:${repoName}] Tool result (${event.tool_name}): ${content.length > 300 ? content.slice(0, 300) + '...' : content}`);
117
+ }
118
+ else if (event.type === 'result') {
119
+ resultText = event.result || '';
120
+ const cost = event.total_cost_usd ? `$${event.total_cost_usd.toFixed(4)}` : 'unknown';
121
+ const duration = event.duration_ms ? `${(event.duration_ms / 1000).toFixed(1)}s` : 'unknown';
122
+ logger.info(`[investigation:${repoName}] Complete | ${resultText.length} chars | ${duration} | cost: ${cost}`);
123
+ }
124
+ }
125
+ catch {
126
+ // Non-JSON line
127
+ if (trimmed.length > 0) {
128
+ logger.debug(`[investigation:${repoName}] ${trimmed.length > 300 ? trimmed.slice(0, 300) + '...' : trimmed}`);
129
+ }
130
+ }
131
+ }
132
+ });
133
+ proc.stderr.on('data', (data) => {
134
+ const chunk = data.toString();
135
+ stderr += chunk;
136
+ const trimmed = chunk.trim();
137
+ if (trimmed) {
138
+ logger.warn(`[investigation:${repoName}] ${trimmed.length > 500 ? trimmed.slice(0, 500) + '...' : trimmed}`);
139
+ }
140
+ });
141
+ proc.on('error', (err) => {
142
+ reject(new Error(`Failed to spawn claude: ${err.message}`));
143
+ });
144
+ proc.on('close', (code) => {
145
+ // Process any remaining data in lineBuffer
146
+ if (lineBuffer.trim()) {
147
+ try {
148
+ const event = JSON.parse(lineBuffer.trim());
149
+ if (event.type === 'result') {
150
+ resultText = event.result || '';
151
+ }
152
+ }
153
+ catch {
154
+ // ignore
155
+ }
156
+ }
157
+ if (code === 0) {
158
+ resolve(resultText || 'Investigation completed but returned no structured result.');
159
+ }
160
+ else {
161
+ const errDetail = stderr.trim() || `exit code ${code}`;
162
+ reject(new Error(`Investigation failed: ${errDetail}`));
163
+ }
164
+ });
165
+ });
166
+ }
167
+ const investigateRepoSchema = zod_1.z.object({
168
+ repoName: zod_1.z.string().describe('Target repo name (must match a configured repo). Available repos depend on DEV_AI_REPOS config.'),
169
+ prompt: zod_1.z.string().describe('What to investigate - bug description, search query, code question, etc.'),
170
+ maxTurns: zod_1.z.number().optional().default(30).describe('Max investigation depth (number of agentic turns). Default: 30.'),
171
+ });
172
+ exports.investigateRepoTool = {
173
+ name: 'investigate_repo',
174
+ group: tool_registry_1.ToolGroup.PLAYGROUND,
175
+ description: 'Spawn a Claude Code agent to investigate code in a local repo. ' +
176
+ 'Uses claude -p (headless mode, subscription-based). ' +
177
+ 'The agent can explore files, search code, analyze the codebase autonomously, ' +
178
+ 'and spawn investigation teams for parallel exploration of complex issues. ' +
179
+ 'Returns the investigation findings as text.',
180
+ schema: investigateRepoSchema,
181
+ async execute(args, _context) {
182
+ const repos = parseRepoPaths();
183
+ // If no repos configured, return helpful error
184
+ if (repos.size === 0) {
185
+ return {
186
+ content: [{
187
+ type: 'text',
188
+ text: 'No repos configured. Set DEV_AI_REPOS in .env.local (format: "name:/path,name2:/path2").',
189
+ }],
190
+ };
191
+ }
192
+ const repoPath = resolveRepoPath(args.repoName);
193
+ if (!repoPath) {
194
+ const available = Array.from(repos.keys()).join(', ');
195
+ return {
196
+ content: [{
197
+ type: 'text',
198
+ text: `Unknown repo: "${args.repoName}". Available repos: ${available}`,
199
+ }],
200
+ };
201
+ }
202
+ const enhancedPrompt = `You are a lead investigator. Use parallel agents to explore the codebase efficiently.
203
+
204
+ HOW TO USE PARALLEL AGENTS:
205
+ - Call multiple Task tools in a SINGLE response to run them in parallel
206
+ - Each Task call blocks until that agent finishes and returns its findings directly to you
207
+ - Use subagent_type: "Explore" with model: "haiku" — these are fast, read-only codebase search agents
208
+ - Do NOT use TeamCreate, SendMessage, or TaskCreate — just call Task directly
209
+
210
+ APPROACH:
211
+ 1. In your FIRST response, call 2-3 Task tools simultaneously, each with a focused search prompt
212
+ 2. You will receive all their findings in the next turn
213
+ 3. If needed, do additional targeted searches yourself with Grep/Read
214
+ 4. Write your FINAL REPORT
215
+
216
+ REPORT FORMAT:
217
+ - Root cause of the issue
218
+ - Affected files with paths and line numbers
219
+ - Suggested fix with specific code changes
220
+
221
+ INVESTIGATION TASK:
222
+ ${args.prompt}`;
223
+ logger.info('Starting investigation', {
224
+ repo: args.repoName,
225
+ repoPath,
226
+ maxTurns: args.maxTurns,
227
+ promptLength: enhancedPrompt.length,
228
+ });
229
+ try {
230
+ const result = await spawnInvestigation(enhancedPrompt, repoPath, args.maxTurns, args.repoName);
231
+ logger.info('Investigation completed', {
232
+ repo: args.repoName,
233
+ resultLength: result.length,
234
+ });
235
+ return {
236
+ content: [{
237
+ type: 'text',
238
+ text: result || 'Investigation completed but returned no output.',
239
+ }],
240
+ };
241
+ }
242
+ catch (error) {
243
+ const errMsg = error instanceof Error ? error.message : String(error);
244
+ logger.error('Investigation failed', { repo: args.repoName, error: errMsg });
245
+ return {
246
+ content: [{
247
+ type: 'text',
248
+ text: `Investigation failed: ${errMsg}`,
249
+ }],
250
+ };
251
+ }
252
+ },
253
+ };
254
+ //# sourceMappingURL=investigate.js.map
@@ -495,22 +495,6 @@ class MCPServerService {
495
495
  catch (err) {
496
496
  this.logger.debug('Could not update .mcp.json', { error: err.message });
497
497
  }
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
498
  resolve();
515
499
  });
516
500
  this.server.on('error', (error) => {
@@ -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
@@ -0,0 +1 @@
1
+ [2026-02-16T05:32:29.204Z] agent-giuseppe-app-builder | skill | hailer-mcp | \"Field '{fieldLabel}' expects Unix ms timestamp (number), got: '{value}'. Convert the date to milliseconds first.\"\n```\n\n**Important:**\n- Read th
@@ -0,0 +1,4 @@
1
+ {"ts":"2026-02-13T08:23:26.264Z","agent":"general-purpose","status":"unknown","project":"hailer-mcp","description":"Convert new agents to OpenCode"}
2
+ {"ts":"2026-02-16T05:26:20.980Z","agent":"agent-svetlana-code-review","status":"unknown","project":"hailer-mcp","description":"Check bot date handling code"}
3
+ {"ts":"2026-02-16T05:32:29.203Z","agent":"agent-giuseppe-app-builder","status":"error","project":"hailer-mcp","description":"Add date validation to bot+tools"}
4
+ {"ts":"2026-02-24T12:50:03.634Z","agent":"agent-gunther-mcp-tools","status":"unknown","project":"hailer-mcp","description":"Find removed MCP tools"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hailer/mcp",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "config": {
5
5
  "docker": {
6
6
  "registry": "registry.gitlab.com/hailer-repos/hailer-mcp"
@@ -25,7 +25,8 @@
25
25
  "release:patch": "npm version patch -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
26
26
  "release:minor": "npm version minor -m 'chore: release v%s' && git push && git push --tags && npm run build && npm publish --access public",
27
27
  "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"
28
+ "seed-config": "tsx src/commands/seed-config.ts",
29
+ "postinstall": "node scripts/postinstall.cjs"
29
30
  },
30
31
  "dependencies": {
31
32
  "@anthropic-ai/sdk": "^0.54.0",
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall: Copies .claude/ (agents, skills, hooks) to the project root.
4
+ * Runs automatically after `npm install @hailer/mcp`.
5
+ * Skips when installing in the hailer-mcp repo itself (development).
6
+ */
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ // Find the project root by walking up from node_modules/@hailer/mcp/
11
+ function findProjectRoot() {
12
+ let dir = __dirname;
13
+ // Walk up until we find a package.json that isn't ours
14
+ for (let i = 0; i < 10; i++) {
15
+ dir = path.dirname(dir);
16
+ const pkgPath = path.join(dir, 'package.json');
17
+ if (fs.existsSync(pkgPath)) {
18
+ try {
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
20
+ // Skip if this is our own package.json
21
+ if (pkg.name === '@hailer/mcp') continue;
22
+ return dir;
23
+ } catch { continue; }
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ function copyDir(src, dest) {
30
+ if (!fs.existsSync(src)) return;
31
+ fs.mkdirSync(dest, { recursive: true });
32
+
33
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
34
+ const srcPath = path.join(src, entry.name);
35
+ const destPath = path.join(dest, entry.name);
36
+
37
+ if (entry.isDirectory()) {
38
+ copyDir(srcPath, destPath);
39
+ } else {
40
+ // Don't overwrite user's local settings
41
+ if (entry.name === 'settings.local.json' && fs.existsSync(destPath)) continue;
42
+ fs.copyFileSync(srcPath, destPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ const projectRoot = findProjectRoot();
48
+
49
+ // Skip if we can't find a project root or if running in dev (our own repo)
50
+ if (!projectRoot) {
51
+ console.log('@hailer/mcp: skipping agent install (no project root found)');
52
+ process.exit(0);
53
+ }
54
+
55
+ const src = path.join(__dirname, '..', '.claude');
56
+ const dest = path.join(projectRoot, '.claude');
57
+
58
+ if (!fs.existsSync(src)) {
59
+ console.log('@hailer/mcp: no .claude/ directory in package, skipping');
60
+ process.exit(0);
61
+ }
62
+
63
+ copyDir(src, dest);
64
+ console.log(`@hailer/mcp: agents installed to ${dest}`);