@hailer/mcp 0.0.1
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/.claude/commands/tool-builder.md +37 -0
- package/.claude/commands/ws-pull.md +44 -0
- package/.claude/settings.json +8 -0
- package/.claude/settings.local.json +49 -0
- package/.claude/skills/activity-api/SKILL.md +96 -0
- package/.claude/skills/activity-api/references/activity-endpoints.md +845 -0
- package/.claude/skills/add-app-member-skill/SKILL.md +977 -0
- package/.claude/skills/agent-building/SKILL.md +243 -0
- package/.claude/skills/agent-building/references/architecture-patterns.md +446 -0
- package/.claude/skills/agent-building/references/code-examples.md +587 -0
- package/.claude/skills/agent-building/references/implementation-guide.md +619 -0
- package/.claude/skills/app-api/SKILL.md +219 -0
- package/.claude/skills/app-api/references/app-endpoints.md +759 -0
- package/.claude/skills/building-hailer-apps-skill/SKILL.md +548 -0
- package/.claude/skills/create-app-skill/SKILL.md +1101 -0
- package/.claude/skills/create-insight-skill/SKILL.md +1317 -0
- package/.claude/skills/get-insight-data-skill/SKILL.md +1053 -0
- package/.claude/skills/hailer-api/SKILL.md +283 -0
- package/.claude/skills/hailer-api/references/activities.md +620 -0
- package/.claude/skills/hailer-api/references/authentication.md +216 -0
- package/.claude/skills/hailer-api/references/datasets.md +437 -0
- package/.claude/skills/hailer-api/references/files.md +301 -0
- package/.claude/skills/hailer-api/references/insights.md +469 -0
- package/.claude/skills/hailer-api/references/workflows.md +720 -0
- package/.claude/skills/hailer-api/references/workspaces-users.md +445 -0
- package/.claude/skills/insight-api/SKILL.md +185 -0
- package/.claude/skills/insight-api/references/insight-endpoints.md +514 -0
- package/.claude/skills/install-workflow-skill/SKILL.md +1056 -0
- package/.claude/skills/list-apps-skill/SKILL.md +1010 -0
- package/.claude/skills/list-workflows-minimal-skill/SKILL.md +992 -0
- package/.claude/skills/local-first-skill/SKILL.md +570 -0
- package/.claude/skills/mcp-tools/SKILL.md +419 -0
- package/.claude/skills/mcp-tools/references/api-endpoints.md +499 -0
- package/.claude/skills/mcp-tools/references/data-structures.md +554 -0
- package/.claude/skills/mcp-tools/references/implementation-patterns.md +717 -0
- package/.claude/skills/preview-insight-skill/SKILL.md +1290 -0
- package/.claude/skills/publish-hailer-app-skill/SKILL.md +453 -0
- package/.claude/skills/remove-app-member-skill/SKILL.md +671 -0
- package/.claude/skills/remove-app-skill/SKILL.md +985 -0
- package/.claude/skills/remove-insight-skill/SKILL.md +1011 -0
- package/.claude/skills/remove-workflow-skill/SKILL.md +920 -0
- package/.claude/skills/scaffold-hailer-app-skill/SKILL.md +1034 -0
- package/.claude/skills/skill-testing/README.md +137 -0
- package/.claude/skills/skill-testing/SKILL.md +348 -0
- package/.claude/skills/skill-testing/references/test-patterns.md +705 -0
- package/.claude/skills/skill-testing/references/testing-guide.md +603 -0
- package/.claude/skills/skill-testing/references/validation-checklist.md +537 -0
- package/.claude/skills/tool-builder/SKILL.md +328 -0
- package/.claude/skills/update-app-skill/SKILL.md +970 -0
- package/.claude/skills/update-workflow-field-skill/SKILL.md +1098 -0
- package/.env.example +81 -0
- package/.mcp.json +13 -0
- package/README.md +297 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +74 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +5 -0
- package/dist/client/adaptive-documentation-bot.d.ts +108 -0
- package/dist/client/adaptive-documentation-bot.js +475 -0
- package/dist/client/adaptive-documentation-types.d.ts +66 -0
- package/dist/client/adaptive-documentation-types.js +9 -0
- package/dist/client/agent-activity-bot.d.ts +51 -0
- package/dist/client/agent-activity-bot.js +166 -0
- package/dist/client/agent-tracker.d.ts +499 -0
- package/dist/client/agent-tracker.js +659 -0
- package/dist/client/description-updater.d.ts +56 -0
- package/dist/client/description-updater.js +259 -0
- package/dist/client/log-parser.d.ts +72 -0
- package/dist/client/log-parser.js +387 -0
- package/dist/client/mcp-client.d.ts +50 -0
- package/dist/client/mcp-client.js +532 -0
- package/dist/client/message-processor.d.ts +35 -0
- package/dist/client/message-processor.js +352 -0
- package/dist/client/multi-bot-manager.d.ts +24 -0
- package/dist/client/multi-bot-manager.js +74 -0
- package/dist/client/providers/anthropic-provider.d.ts +19 -0
- package/dist/client/providers/anthropic-provider.js +631 -0
- package/dist/client/providers/llm-provider.d.ts +47 -0
- package/dist/client/providers/llm-provider.js +367 -0
- package/dist/client/providers/openai-provider.d.ts +23 -0
- package/dist/client/providers/openai-provider.js +621 -0
- package/dist/client/simple-llm-caller.d.ts +19 -0
- package/dist/client/simple-llm-caller.js +100 -0
- package/dist/client/skill-generator.d.ts +81 -0
- package/dist/client/skill-generator.js +386 -0
- package/dist/client/test-adaptive-bot.d.ts +9 -0
- package/dist/client/test-adaptive-bot.js +82 -0
- package/dist/client/token-pricing.d.ts +38 -0
- package/dist/client/token-pricing.js +127 -0
- package/dist/client/token-tracker.d.ts +232 -0
- package/dist/client/token-tracker.js +457 -0
- package/dist/client/token-usage-bot.d.ts +53 -0
- package/dist/client/token-usage-bot.js +153 -0
- package/dist/client/tool-executor.d.ts +69 -0
- package/dist/client/tool-executor.js +159 -0
- package/dist/client/tool-schema-loader.d.ts +60 -0
- package/dist/client/tool-schema-loader.js +178 -0
- package/dist/client/types.d.ts +69 -0
- package/dist/client/types.js +7 -0
- package/dist/config.d.ts +162 -0
- package/dist/config.js +296 -0
- package/dist/core.d.ts +26 -0
- package/dist/core.js +147 -0
- package/dist/lib/context-manager.d.ts +111 -0
- package/dist/lib/context-manager.js +431 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +277 -0
- package/dist/lib/materialize.d.ts +3 -0
- package/dist/lib/materialize.js +101 -0
- package/dist/lib/normalizedName.d.ts +7 -0
- package/dist/lib/normalizedName.js +48 -0
- package/dist/lib/prompt-length-manager.d.ts +81 -0
- package/dist/lib/prompt-length-manager.js +457 -0
- package/dist/lib/terminal-prompt.d.ts +9 -0
- package/dist/lib/terminal-prompt.js +108 -0
- package/dist/mcp/UserContextCache.d.ts +56 -0
- package/dist/mcp/UserContextCache.js +163 -0
- package/dist/mcp/auth.d.ts +2 -0
- package/dist/mcp/auth.js +29 -0
- package/dist/mcp/hailer-clients.d.ts +42 -0
- package/dist/mcp/hailer-clients.js +246 -0
- package/dist/mcp/signal-handler.d.ts +45 -0
- package/dist/mcp/signal-handler.js +317 -0
- package/dist/mcp/tool-registry.d.ts +100 -0
- package/dist/mcp/tool-registry.js +306 -0
- package/dist/mcp/tools/activity.d.ts +15 -0
- package/dist/mcp/tools/activity.js +955 -0
- package/dist/mcp/tools/app.d.ts +20 -0
- package/dist/mcp/tools/app.js +1488 -0
- package/dist/mcp/tools/discussion.d.ts +19 -0
- package/dist/mcp/tools/discussion.js +950 -0
- package/dist/mcp/tools/file.d.ts +15 -0
- package/dist/mcp/tools/file.js +119 -0
- package/dist/mcp/tools/insight.d.ts +17 -0
- package/dist/mcp/tools/insight.js +806 -0
- package/dist/mcp/tools/skill.d.ts +10 -0
- package/dist/mcp/tools/skill.js +279 -0
- package/dist/mcp/tools/user.d.ts +10 -0
- package/dist/mcp/tools/user.js +108 -0
- package/dist/mcp/tools/workflow-template.d.ts +19 -0
- package/dist/mcp/tools/workflow-template.js +822 -0
- package/dist/mcp/tools/workflow.d.ts +18 -0
- package/dist/mcp/tools/workflow.js +1362 -0
- package/dist/mcp/utils/api-errors.d.ts +45 -0
- package/dist/mcp/utils/api-errors.js +160 -0
- package/dist/mcp/utils/data-transformers.d.ts +102 -0
- package/dist/mcp/utils/data-transformers.js +194 -0
- package/dist/mcp/utils/file-upload.d.ts +33 -0
- package/dist/mcp/utils/file-upload.js +148 -0
- package/dist/mcp/utils/hailer-api-client.d.ts +120 -0
- package/dist/mcp/utils/hailer-api-client.js +323 -0
- package/dist/mcp/utils/index.d.ts +13 -0
- package/dist/mcp/utils/index.js +39 -0
- package/dist/mcp/utils/logger.d.ts +42 -0
- package/dist/mcp/utils/logger.js +103 -0
- package/dist/mcp/utils/types.d.ts +286 -0
- package/dist/mcp/utils/types.js +7 -0
- package/dist/mcp/workspace-cache.d.ts +42 -0
- package/dist/mcp/workspace-cache.js +97 -0
- package/dist/mcp-server.d.ts +42 -0
- package/dist/mcp-server.js +280 -0
- package/package.json +56 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# Implementation Patterns
|
|
2
|
+
|
|
3
|
+
Step-by-step guides for common MCP tool development tasks.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Pattern 1: Adding a New API Endpoint
|
|
8
|
+
|
|
9
|
+
Follow this pattern when you need to call a new Hailer API endpoint.
|
|
10
|
+
|
|
11
|
+
### Step 1: Add Function to `hailer-api.ts`
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// server/src/lib/hailer-api.ts
|
|
15
|
+
|
|
16
|
+
export async function myNewEndpoint(
|
|
17
|
+
client: HailerClient,
|
|
18
|
+
arg1: string,
|
|
19
|
+
options: MyOptions = {}
|
|
20
|
+
): Promise<MyResponse> {
|
|
21
|
+
const { option1, option2 = 'default' } = options;
|
|
22
|
+
|
|
23
|
+
const response = await fetch(
|
|
24
|
+
`${client.baseUrl}/v3/my/endpoint`,
|
|
25
|
+
{
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'hlrkey': client.authToken
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify([arg1, { option1, option2 }])
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorText = await response.text();
|
|
37
|
+
throw new Error(`API error (${response.status}): ${errorText}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Key Points**:
|
|
45
|
+
- Always use JSON array format: `[arg1, arg2, ...]`
|
|
46
|
+
- Include `hlrkey` header with auth token
|
|
47
|
+
- Check `response.ok` before parsing JSON
|
|
48
|
+
- Provide helpful error messages with status code
|
|
49
|
+
|
|
50
|
+
### Step 2: Test the Endpoint
|
|
51
|
+
|
|
52
|
+
Before building MCP tool, test the API function directly:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// In a test file or debug route
|
|
56
|
+
const result = await myNewEndpoint(
|
|
57
|
+
context.client,
|
|
58
|
+
"test-arg",
|
|
59
|
+
{ option1: "value" }
|
|
60
|
+
);
|
|
61
|
+
console.log('Result:', result);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 3: Document the Endpoint
|
|
65
|
+
|
|
66
|
+
Add to `api-endpoints.md` with full request/response examples.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Pattern 2: Creating a New MCP Tool
|
|
71
|
+
|
|
72
|
+
Once your API endpoint works, wrap it in an MCP tool.
|
|
73
|
+
|
|
74
|
+
### Step 1: Add Static Method to Tool Class
|
|
75
|
+
|
|
76
|
+
Choose the appropriate class:
|
|
77
|
+
- **ReadTools.ts** - Read-only operations (list, get, search)
|
|
78
|
+
- **WriteTools.ts** - Write operations (create, update, delete)
|
|
79
|
+
- **WorkflowTools.ts** - Workflow metadata operations
|
|
80
|
+
- **WorkspaceTools.ts** - Workspace operations
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// server/src/mcp/tools/ReadTools.ts
|
|
84
|
+
|
|
85
|
+
export class ReadTools {
|
|
86
|
+
static async my_new_tool(
|
|
87
|
+
context: UserContext,
|
|
88
|
+
args: { arg1: string; option1?: string }
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
try {
|
|
91
|
+
const { arg1, option1 } = args;
|
|
92
|
+
|
|
93
|
+
// Validate required arguments
|
|
94
|
+
if (!arg1) {
|
|
95
|
+
return "Error: arg1 is required";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Call API function
|
|
99
|
+
const result = await myNewEndpoint(
|
|
100
|
+
context.client,
|
|
101
|
+
arg1,
|
|
102
|
+
{ option1 }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Format response for LLM
|
|
106
|
+
return formatMyResponse(result);
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Error in my_new_tool:', error);
|
|
110
|
+
return `Error: ${error.message}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Key Points**:
|
|
117
|
+
- Always wrap in try/catch
|
|
118
|
+
- Validate required arguments upfront
|
|
119
|
+
- Return user-friendly error messages
|
|
120
|
+
- Log errors for debugging
|
|
121
|
+
- Format response for LLM readability
|
|
122
|
+
|
|
123
|
+
### Step 2: Format Response
|
|
124
|
+
|
|
125
|
+
Create a helper function to format API response:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
function formatMyResponse(data: MyResponse): string {
|
|
129
|
+
if (!data || data.items.length === 0) {
|
|
130
|
+
return "No items found.";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lines = [`Found ${data.items.length} items:\n`];
|
|
134
|
+
|
|
135
|
+
for (const item of data.items) {
|
|
136
|
+
lines.push(`- ${item.name} (ID: ${item._id})`);
|
|
137
|
+
if (item.description) {
|
|
138
|
+
lines.push(` ${item.description}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Formatting Tips**:
|
|
147
|
+
- Start with summary count
|
|
148
|
+
- Use bullet points for lists
|
|
149
|
+
- Include relevant IDs for follow-up actions
|
|
150
|
+
- Keep it concise but informative
|
|
151
|
+
- Use markdown formatting
|
|
152
|
+
|
|
153
|
+
### Step 3: Register Tool in GlobalToolRegistry
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// server/src/mcp/GlobalToolRegistry.ts
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
name: "my_new_tool",
|
|
160
|
+
description: "Clear description of what this tool does and when to use it",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
arg1: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Description of arg1"
|
|
167
|
+
},
|
|
168
|
+
option1: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Optional parameter description"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: ["arg1"]
|
|
174
|
+
},
|
|
175
|
+
execute: async (args: any, context: UserContext) => {
|
|
176
|
+
return ReadTools.my_new_tool(context, args);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Description Guidelines**:
|
|
182
|
+
- Be specific about what it does
|
|
183
|
+
- Mention key parameters
|
|
184
|
+
- Indicate when LLM should use it
|
|
185
|
+
- Include examples if complex
|
|
186
|
+
|
|
187
|
+
### Step 4: Test the Tool
|
|
188
|
+
|
|
189
|
+
Test via debug endpoint:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
curl -X POST http://localhost:3030/api/debug/execute-tool \
|
|
193
|
+
-H "Content-Type: application/json" \
|
|
194
|
+
-d '{
|
|
195
|
+
"apiKey": "test-key",
|
|
196
|
+
"toolName": "my_new_tool",
|
|
197
|
+
"args": {
|
|
198
|
+
"arg1": "test-value",
|
|
199
|
+
"option1": "optional-value"
|
|
200
|
+
}
|
|
201
|
+
}'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Or test with Claude Code:
|
|
205
|
+
```
|
|
206
|
+
Use my_new_tool with arg1="test"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Pattern 3: Implementing Filters
|
|
212
|
+
|
|
213
|
+
Advanced filtering for activity queries.
|
|
214
|
+
|
|
215
|
+
### Basic Filter
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const activities = await listActivities(client, workflowId, {
|
|
219
|
+
filters: {
|
|
220
|
+
and: [
|
|
221
|
+
{ "priority": { equalTo: "high" } }
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
returnFlat: true
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Multiple Conditions (AND)
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
filters: {
|
|
232
|
+
and: [
|
|
233
|
+
{ "status": { equalTo: "active" } },
|
|
234
|
+
{ "priority": { equalTo: "high" } },
|
|
235
|
+
{ "created": { greaterThan: "2025-10-01" } }
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Multiple Conditions (OR)
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
filters: {
|
|
244
|
+
or: [
|
|
245
|
+
{ "status": { equalTo: "active" } },
|
|
246
|
+
{ "status": { equalTo: "pending" } }
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Range Filters
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Date range
|
|
255
|
+
filters: {
|
|
256
|
+
and: [
|
|
257
|
+
{ "created": { between: ["2025-10-01", "2025-10-31"] } }
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Numeric range
|
|
262
|
+
filters: {
|
|
263
|
+
and: [
|
|
264
|
+
{ "budget": { greaterThanOrEqual: 10000 } },
|
|
265
|
+
{ "budget": { lessThanOrEqual: 50000 } }
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Text Search
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
filters: {
|
|
274
|
+
and: [
|
|
275
|
+
{ "description": { textSearch: "urgent" } }
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Filter by Relationship
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Find all tasks for a specific topic
|
|
284
|
+
filters: {
|
|
285
|
+
and: [
|
|
286
|
+
{ "relatedTo": { equalTo: topicActivityId } }
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Complex Filter Example
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Find high-priority active tasks created this month
|
|
295
|
+
const filters = {
|
|
296
|
+
and: [
|
|
297
|
+
{ "priority": { equalTo: "high" } },
|
|
298
|
+
{ "status": { equalTo: "active" } },
|
|
299
|
+
{ "created": {
|
|
300
|
+
between: [
|
|
301
|
+
new Date('2025-10-01').getTime().toString(),
|
|
302
|
+
new Date('2025-10-31').getTime().toString()
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Pattern 4: Implementing Pagination
|
|
313
|
+
|
|
314
|
+
Handle large result sets efficiently.
|
|
315
|
+
|
|
316
|
+
### Simple Pagination
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
async function getAllActivities(
|
|
320
|
+
client: HailerClient,
|
|
321
|
+
workflowId: string
|
|
322
|
+
): Promise<Activity[]> {
|
|
323
|
+
const allActivities: Activity[] = [];
|
|
324
|
+
let page = 1;
|
|
325
|
+
let hasMore = true;
|
|
326
|
+
|
|
327
|
+
while (hasMore) {
|
|
328
|
+
const response = await listActivities(client, workflowId, {
|
|
329
|
+
page,
|
|
330
|
+
limit: 100,
|
|
331
|
+
returnFlat: true
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
allActivities.push(...response.data);
|
|
335
|
+
|
|
336
|
+
hasMore = page < response.totalPages;
|
|
337
|
+
page++;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return allActivities;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Pagination with Progress
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
async function getAllActivitiesWithProgress(
|
|
348
|
+
client: HailerClient,
|
|
349
|
+
workflowId: string,
|
|
350
|
+
onProgress?: (current: number, total: number) => void
|
|
351
|
+
): Promise<Activity[]> {
|
|
352
|
+
const allActivities: Activity[] = [];
|
|
353
|
+
let page = 1;
|
|
354
|
+
|
|
355
|
+
// Get first page to know total
|
|
356
|
+
const firstResponse = await listActivities(client, workflowId, {
|
|
357
|
+
page: 1,
|
|
358
|
+
limit: 100,
|
|
359
|
+
returnFlat: true
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
allActivities.push(...firstResponse.data);
|
|
363
|
+
const totalPages = firstResponse.totalPages;
|
|
364
|
+
|
|
365
|
+
onProgress?.(1, totalPages);
|
|
366
|
+
|
|
367
|
+
// Fetch remaining pages
|
|
368
|
+
for (page = 2; page <= totalPages; page++) {
|
|
369
|
+
const response = await listActivities(client, workflowId, {
|
|
370
|
+
page,
|
|
371
|
+
limit: 100,
|
|
372
|
+
returnFlat: true
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
allActivities.push(...response.data);
|
|
376
|
+
onProgress?.(page, totalPages);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return allActivities;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Discussion Message Pagination
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
async function getAllMessages(
|
|
387
|
+
client: HailerClient,
|
|
388
|
+
discussionId: string
|
|
389
|
+
): Promise<Message[]> {
|
|
390
|
+
const allMessages: Message[] = [];
|
|
391
|
+
|
|
392
|
+
// Fetch latest 50
|
|
393
|
+
const latest = await fetchLatestMessages(client, discussionId, 50);
|
|
394
|
+
allMessages.push(...latest.messages);
|
|
395
|
+
|
|
396
|
+
// Fetch older messages
|
|
397
|
+
let oldestId = latest.messages[latest.messages.length - 1]?._id;
|
|
398
|
+
|
|
399
|
+
while (oldestId) {
|
|
400
|
+
const previous = await fetchPreviousMessages(client, oldestId, 50);
|
|
401
|
+
|
|
402
|
+
if (previous.messages.length === 0) {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
allMessages.push(...previous.messages);
|
|
407
|
+
oldestId = previous.messages[previous.messages.length - 1]?._id;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return allMessages;
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Pattern 5: Socket.io Operations
|
|
417
|
+
|
|
418
|
+
Working with socket.io methods via `@hailer/cli`.
|
|
419
|
+
|
|
420
|
+
### Basic Socket Call
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
async function followActivity(
|
|
424
|
+
client: HailerClient,
|
|
425
|
+
activityId: string,
|
|
426
|
+
follow: boolean
|
|
427
|
+
): Promise<void> {
|
|
428
|
+
await client.socket.call('activities.follow', {
|
|
429
|
+
activityId,
|
|
430
|
+
follow
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Socket Call with Response
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
async function getActivityDetails(
|
|
439
|
+
client: HailerClient,
|
|
440
|
+
activityId: string
|
|
441
|
+
): Promise<any> {
|
|
442
|
+
const response = await client.socket.call('activities.get', {
|
|
443
|
+
activityId
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return response;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Socket Call with Error Handling
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
async function leaveDiscussion(
|
|
454
|
+
client: HailerClient,
|
|
455
|
+
discussionId: string
|
|
456
|
+
): Promise<string> {
|
|
457
|
+
try {
|
|
458
|
+
await client.socket.call('messenger.leave_discussion', {
|
|
459
|
+
discussionId
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return `Successfully left discussion ${discussionId}`;
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error('Socket error:', error);
|
|
465
|
+
return `Error leaving discussion: ${error.message}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Pattern 6: Error Handling Best Practices
|
|
473
|
+
|
|
474
|
+
Comprehensive error handling for robust tools.
|
|
475
|
+
|
|
476
|
+
### Multi-Level Error Handling
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
export class ReadTools {
|
|
480
|
+
static async list_activities(
|
|
481
|
+
context: UserContext,
|
|
482
|
+
args: any
|
|
483
|
+
): Promise<string> {
|
|
484
|
+
try {
|
|
485
|
+
// Level 1: Validate arguments
|
|
486
|
+
if (!args.workflowId) {
|
|
487
|
+
return "Error: workflowId is required";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Level 2: Validate workflow exists
|
|
491
|
+
const workflow = await getWorkflow(context.client, args.workflowId);
|
|
492
|
+
if (!workflow) {
|
|
493
|
+
return `Error: Workflow ${args.workflowId} not found`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Level 3: Call API
|
|
497
|
+
const activities = await listActivities(
|
|
498
|
+
context.client,
|
|
499
|
+
args.workflowId,
|
|
500
|
+
{ returnFlat: true }
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// Level 4: Validate response
|
|
504
|
+
if (!activities || !activities.data) {
|
|
505
|
+
return "Error: Invalid response from API";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return formatActivities(activities.data);
|
|
509
|
+
|
|
510
|
+
} catch (error) {
|
|
511
|
+
// Level 5: Catch unexpected errors
|
|
512
|
+
console.error('Error in list_activities:', error);
|
|
513
|
+
|
|
514
|
+
// Provide helpful error message based on error type
|
|
515
|
+
if (error.message.includes('401')) {
|
|
516
|
+
return "Error: Authentication failed. Check API credentials.";
|
|
517
|
+
}
|
|
518
|
+
if (error.message.includes('404')) {
|
|
519
|
+
return "Error: Endpoint not found. Check API version.";
|
|
520
|
+
}
|
|
521
|
+
if (error.message.includes('timeout')) {
|
|
522
|
+
return "Error: Request timed out. Try again.";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return `Error: ${error.message}`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Specific Error Types
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
class HailerAPIError extends Error {
|
|
535
|
+
constructor(
|
|
536
|
+
public statusCode: number,
|
|
537
|
+
public details: any,
|
|
538
|
+
message: string
|
|
539
|
+
) {
|
|
540
|
+
super(message);
|
|
541
|
+
this.name = 'HailerAPIError';
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Usage in API function
|
|
546
|
+
if (!response.ok) {
|
|
547
|
+
const details = await response.json().catch(() => ({}));
|
|
548
|
+
throw new HailerAPIError(
|
|
549
|
+
response.status,
|
|
550
|
+
details,
|
|
551
|
+
`API request failed: ${response.statusText}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Catch specific error type
|
|
556
|
+
try {
|
|
557
|
+
await apiCall();
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof HailerAPIError) {
|
|
560
|
+
if (error.statusCode === 422) {
|
|
561
|
+
return `Validation error: ${JSON.stringify(error.details)}`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Pattern 7: Testing Tools
|
|
571
|
+
|
|
572
|
+
Test tools thoroughly before deployment.
|
|
573
|
+
|
|
574
|
+
### Unit Test Pattern
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// server/src/mcp/tools/__tests__/ReadTools.test.ts
|
|
578
|
+
|
|
579
|
+
import { ReadTools } from '../ReadTools';
|
|
580
|
+
import { UserContext } from '../../UserContextCache';
|
|
581
|
+
|
|
582
|
+
describe('ReadTools', () => {
|
|
583
|
+
let mockContext: UserContext;
|
|
584
|
+
|
|
585
|
+
beforeEach(() => {
|
|
586
|
+
mockContext = {
|
|
587
|
+
client: createMockClient(),
|
|
588
|
+
initData: mockInitData,
|
|
589
|
+
workspaceCache: mockCache
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should list activities', async () => {
|
|
594
|
+
const result = await ReadTools.list_activities(
|
|
595
|
+
mockContext,
|
|
596
|
+
{ workflowId: 'test-workflow-id' }
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
expect(result).toContain('Found');
|
|
600
|
+
expect(result).not.toContain('Error');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should handle missing workflowId', async () => {
|
|
604
|
+
const result = await ReadTools.list_activities(
|
|
605
|
+
mockContext,
|
|
606
|
+
{}
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(result).toContain('Error');
|
|
610
|
+
expect(result).toContain('workflowId is required');
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Integration Test Pattern
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Test against real API (local development)
|
|
619
|
+
describe('ReadTools Integration', () => {
|
|
620
|
+
it('should fetch real activities', async () => {
|
|
621
|
+
const context = await createRealContext('test-key');
|
|
622
|
+
|
|
623
|
+
const result = await ReadTools.list_activities(
|
|
624
|
+
context,
|
|
625
|
+
{ workflowId: '68446dc05b30685f67c6fcd4' }
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
console.log('Result:', result);
|
|
629
|
+
expect(result).not.toContain('Error');
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Manual Test via Debug Endpoint
|
|
635
|
+
|
|
636
|
+
```bash
|
|
637
|
+
#!/bin/bash
|
|
638
|
+
# test-tool.sh
|
|
639
|
+
|
|
640
|
+
curl -X POST http://localhost:3030/api/debug/execute-tool \
|
|
641
|
+
-H "Content-Type: application/json" \
|
|
642
|
+
-d "{
|
|
643
|
+
\"apiKey\": \"test-key\",
|
|
644
|
+
\"toolName\": \"$1\",
|
|
645
|
+
\"args\": $2
|
|
646
|
+
}" | jq
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Usage:
|
|
650
|
+
```bash
|
|
651
|
+
./test-tool.sh list_activities '{"workflowId":"68446dc05b30685f67c6fcd4"}'
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Pattern 8: Optimizing for Context
|
|
657
|
+
|
|
658
|
+
Keep MCP tool responses concise to save context.
|
|
659
|
+
|
|
660
|
+
### Bad: Verbose Response
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
return `Successfully listed activities. Total count: ${activities.length}
|
|
664
|
+
Activities:
|
|
665
|
+
${activities.map(a => `
|
|
666
|
+
- ID: ${a._id}
|
|
667
|
+
- Name: ${a.name}
|
|
668
|
+
- Process ID: ${a.processId}
|
|
669
|
+
- Phase ID: ${a.phaseId}
|
|
670
|
+
- Created: ${a.created}
|
|
671
|
+
- Updated: ${a.updated}
|
|
672
|
+
- Discussion: ${a.discussion}
|
|
673
|
+
- Fields: ${JSON.stringify(a.fields, null, 2)}
|
|
674
|
+
`).join('\n')}`;
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Good: Concise Response
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
const summary = activities.length === 0
|
|
681
|
+
? "No activities found."
|
|
682
|
+
: `Found ${activities.length} activities:`;
|
|
683
|
+
|
|
684
|
+
const items = activities.slice(0, 10).map(a =>
|
|
685
|
+
`- ${a.name} (${a._id})`
|
|
686
|
+
).join('\n');
|
|
687
|
+
|
|
688
|
+
const more = activities.length > 10
|
|
689
|
+
? `\n... and ${activities.length - 10} more`
|
|
690
|
+
: '';
|
|
691
|
+
|
|
692
|
+
return `${summary}\n${items}${more}`;
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Include Only What's Needed
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
// For listing: names and IDs only
|
|
699
|
+
// For details: full data when requested
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Checklist: New MCP Tool
|
|
705
|
+
|
|
706
|
+
- [ ] API endpoint function in `hailer-api.ts`
|
|
707
|
+
- [ ] Test API function directly
|
|
708
|
+
- [ ] Static method in appropriate tool class
|
|
709
|
+
- [ ] Input validation
|
|
710
|
+
- [ ] Error handling (try/catch)
|
|
711
|
+
- [ ] User-friendly response formatting
|
|
712
|
+
- [ ] Tool registration in GlobalToolRegistry
|
|
713
|
+
- [ ] Clear description in schema
|
|
714
|
+
- [ ] Test via debug endpoint
|
|
715
|
+
- [ ] Test with Claude Code
|
|
716
|
+
- [ ] Document in skill if complex
|
|
717
|
+
- [ ] Update CLAUDE.md if adds new capability
|