@hailer/mcp 1.1.13 → 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/package.json +1 -1
- package/scripts/postinstall.cjs +64 -0
- package/scripts/test-hal-tools.ts +154 -0
package/package.json
CHANGED
|
@@ -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}`);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env npx ts-node
|
|
2
|
+
/**
|
|
3
|
+
* HAL Tools Test - Simple version
|
|
4
|
+
* Just calls each tool and shows the result. No magic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as dotenv from 'dotenv';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env.local') });
|
|
11
|
+
|
|
12
|
+
const MCP_URL = process.env.MCP_SERVER_URL || 'http://localhost:3030/api/mcp';
|
|
13
|
+
|
|
14
|
+
async function call(apiKey: string, tool: string, args: Record<string, unknown> = {}) {
|
|
15
|
+
console.log(`\n>>> ${tool}`);
|
|
16
|
+
console.log(` args: ${JSON.stringify(args)}`);
|
|
17
|
+
|
|
18
|
+
const res = await fetch(`${MCP_URL}?apiKey=${apiKey}`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
jsonrpc: '2.0',
|
|
23
|
+
id: '1',
|
|
24
|
+
method: 'tools/call',
|
|
25
|
+
params: { name: tool, arguments: args },
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
for (const line of text.split('\n')) {
|
|
31
|
+
if (line.startsWith('data: ')) {
|
|
32
|
+
const data = JSON.parse(line.substring(6));
|
|
33
|
+
if (data.error) {
|
|
34
|
+
console.log(`<<< ERROR: ${data.error.message}`);
|
|
35
|
+
return { error: true, data: null };
|
|
36
|
+
}
|
|
37
|
+
const content = data.result?.content?.[0]?.text || '';
|
|
38
|
+
// Show first 500 chars
|
|
39
|
+
console.log(`<<< ${content.substring(0, 500)}${content.length > 500 ? '...' : ''}`);
|
|
40
|
+
return { error: false, content };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log('<<< NO RESPONSE');
|
|
44
|
+
return { error: true, data: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const apiKey = process.env.MCP_CLIENT_API_KEY;
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
console.error('Set MCP_CLIENT_API_KEY in .env.local');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('=== HAL TOOLS TEST ===\n');
|
|
55
|
+
|
|
56
|
+
// Get a workflow to work with
|
|
57
|
+
const wf = await call(apiKey, 'list_workflows_minimal', {});
|
|
58
|
+
const wfMatch = wf.content?.match(/`([a-f0-9]{24})`/);
|
|
59
|
+
const workflowId = wfMatch?.[1];
|
|
60
|
+
console.log(`\n[Using workflowId: ${workflowId}]`);
|
|
61
|
+
|
|
62
|
+
if (!workflowId) {
|
|
63
|
+
console.log('No workflow found, stopping');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get phase
|
|
68
|
+
const ph = await call(apiKey, 'list_workflow_phases', { workflowId });
|
|
69
|
+
const phMatch = ph.content?.match(/`([a-f0-9]{24})`/);
|
|
70
|
+
const phaseId = phMatch?.[1];
|
|
71
|
+
console.log(`\n[Using phaseId: ${phaseId}]`);
|
|
72
|
+
|
|
73
|
+
// Schema
|
|
74
|
+
await call(apiKey, 'get_workflow_schema', { workflowId, phaseId });
|
|
75
|
+
|
|
76
|
+
// List activities
|
|
77
|
+
await call(apiKey, 'list_activities', { workflowId, phaseId, fields: '[]' });
|
|
78
|
+
|
|
79
|
+
// Count
|
|
80
|
+
await call(apiKey, 'count_activities', { workflowId });
|
|
81
|
+
|
|
82
|
+
// Create activity
|
|
83
|
+
const created = await call(apiKey, 'create_activity', {
|
|
84
|
+
workflowId,
|
|
85
|
+
name: `TEST_${Date.now()}`
|
|
86
|
+
});
|
|
87
|
+
const actMatch = created.content?.match(/\*\*ID\*\*:\s*([a-f0-9]{24})/);
|
|
88
|
+
const activityId = actMatch?.[1];
|
|
89
|
+
console.log(`\n[Created activityId: ${activityId}]`);
|
|
90
|
+
|
|
91
|
+
if (activityId) {
|
|
92
|
+
// Show it
|
|
93
|
+
await call(apiKey, 'show_activity_by_id', { activityId });
|
|
94
|
+
|
|
95
|
+
// Update it
|
|
96
|
+
await call(apiKey, 'update_activity', { activityId, name: `UPDATED_${Date.now()}` });
|
|
97
|
+
|
|
98
|
+
// Delete it (this tool doesn't exist yet)
|
|
99
|
+
await call(apiKey, 'delete_activity', { activityId });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Insights
|
|
103
|
+
await call(apiKey, 'list_insights', {});
|
|
104
|
+
|
|
105
|
+
const sources = [{
|
|
106
|
+
name: 'src',
|
|
107
|
+
workflowId,
|
|
108
|
+
fields: [{ meta: 'name', name: 'Name', as: 'name' }]
|
|
109
|
+
}];
|
|
110
|
+
|
|
111
|
+
await call(apiKey, 'preview_insight', { sources, query: 'SELECT * FROM src LIMIT 3' });
|
|
112
|
+
|
|
113
|
+
const ins = await call(apiKey, 'create_insight', {
|
|
114
|
+
name: `TEST_${Date.now()}`,
|
|
115
|
+
sources,
|
|
116
|
+
query: 'SELECT * FROM src LIMIT 3'
|
|
117
|
+
});
|
|
118
|
+
const insMatch = ins.content?.match(/`([a-f0-9]{24})`/);
|
|
119
|
+
const insightId = insMatch?.[1];
|
|
120
|
+
|
|
121
|
+
if (insightId) {
|
|
122
|
+
await call(apiKey, 'get_insight_data', { insightId });
|
|
123
|
+
await call(apiKey, 'remove_insight', { insightId });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Discussions
|
|
127
|
+
await call(apiKey, 'list_my_discussions', {});
|
|
128
|
+
|
|
129
|
+
if (activityId) {
|
|
130
|
+
await call(apiKey, 'join_discussion', { activityId });
|
|
131
|
+
// Get discussion from activity
|
|
132
|
+
const act = await call(apiKey, 'show_activity_by_id', { activityId });
|
|
133
|
+
const discMatch = act.content?.match(/discussion.*?([a-f0-9]{24})/i);
|
|
134
|
+
const discussionId = discMatch?.[1];
|
|
135
|
+
|
|
136
|
+
if (discussionId) {
|
|
137
|
+
await call(apiKey, 'fetch_discussion_messages', { discussionId });
|
|
138
|
+
await call(apiKey, 'add_discussion_message', { discussionId, content: 'TEST' });
|
|
139
|
+
await call(apiKey, 'get_activity_from_discussion', { discussionId });
|
|
140
|
+
await call(apiKey, 'leave_discussion', { discussionId });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Other
|
|
145
|
+
await call(apiKey, 'list_workflows', {});
|
|
146
|
+
await call(apiKey, 'list_apps', {});
|
|
147
|
+
await call(apiKey, 'list_templates', {});
|
|
148
|
+
await call(apiKey, 'get_workspace_balance', {});
|
|
149
|
+
await call(apiKey, 'search_workspace_users', { query: 'test' });
|
|
150
|
+
|
|
151
|
+
console.log('\n=== DONE ===');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main().catch(console.error);
|