Skip to content

Commit eabb1f0

Browse files
committed
feat(shared): enhance error handling and make parameters optional in MCP tools
1 parent 34df6ec commit eabb1f0

File tree

4 files changed

+197
-51
lines changed

4 files changed

+197
-51
lines changed

packages/shared/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
"import": "./dist/es/mcp/index.mjs",
6363
"require": "./dist/lib/mcp/index.js"
6464
},
65+
"./logger": {
66+
"types": "./dist/types/logger.d.ts",
67+
"import": "./dist/es/logger.mjs",
68+
"require": "./dist/lib/logger.js"
69+
},
6570
"./*": {
6671
"types": "./dist/types/*.d.ts",
6772
"import": "./dist/es/*.mjs",

packages/shared/src/mcp/base-server.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,21 @@ export abstract class BaseMCPServer {
126126
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
127127
}
128128

129+
// Setup process-level error handlers to prevent crashes
130+
process.on('uncaughtException', (error: Error) => {
131+
console.error(`[${this.config.name}] Uncaught Exception:`, error);
132+
console.error('Stack:', error.stack);
133+
// Don't exit - try to recover
134+
});
135+
136+
process.on('unhandledRejection', (reason: any) => {
137+
console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
138+
if (reason instanceof Error) {
139+
console.error('Stack:', reason.stack);
140+
}
141+
// Don't exit - try to recover
142+
});
143+
129144
// Setup cleanup handlers
130145
process.stdin.on('close', () => this.performCleanup());
131146

packages/shared/src/mcp/tool-generator.ts

Lines changed: 176 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,30 @@ export function generateToolsFromActionSpace(
2222
paramSchema._def?.typeName === 'ZodObject' &&
2323
'shape' in paramSchema
2424
) {
25-
schema = (paramSchema as z.ZodObject<z.ZodRawShape>).shape;
25+
const originalShape = (paramSchema as z.ZodObject<z.ZodRawShape>).shape;
26+
27+
// Deep clone and modify the shape to make locate.prompt optional
28+
schema = Object.fromEntries(
29+
Object.entries(originalShape).map(([key, value]) => {
30+
// Check if this is a locate field (contains prompt field)
31+
if (
32+
value &&
33+
typeof value === 'object' &&
34+
'_def' in value &&
35+
(value as any)._def?.typeName === 'ZodObject' &&
36+
'shape' in value
37+
) {
38+
const fieldShape = (value as any).shape;
39+
if ('prompt' in fieldShape) {
40+
// This is a locate field, make prompt optional
41+
const newFieldShape = { ...fieldShape };
42+
newFieldShape.prompt = fieldShape.prompt.optional();
43+
return [key, z.object(newFieldShape).passthrough()];
44+
}
45+
}
46+
return [key, value];
47+
}),
48+
);
2649
} else {
2750
// Otherwise use it as-is
2851
schema = paramSchema as unknown as Record<string, z.ZodTypeAny>;
@@ -34,44 +57,117 @@ export function generateToolsFromActionSpace(
3457
description: action.description || `Execute ${action.name} action`,
3558
schema,
3659
handler: async (args: Record<string, unknown>) => {
37-
const agent = await getAgent();
60+
try {
61+
const agent = await getAgent();
3862

39-
// Call the action through agent's aiAction method
40-
// args already contains the unwrapped parameters (e.g., { locate: {...} })
41-
if (agent.aiAction) {
42-
await agent.aiAction(`Use the action "${action.name}"`, {
43-
...args,
44-
});
45-
}
63+
// Call the action through agent's aiAction method
64+
// args already contains the unwrapped parameters (e.g., { locate: {...} })
65+
if (agent.aiAction) {
66+
// Convert args object to natural language description
67+
let argsDescription = '';
68+
try {
69+
argsDescription = Object.entries(args)
70+
.map(([key, value]) => {
71+
if (typeof value === 'object' && value !== null) {
72+
try {
73+
return `${key}: ${JSON.stringify(value)}`;
74+
} catch {
75+
return `${key}: [object]`;
76+
}
77+
}
78+
return `${key}: "${value}"`;
79+
})
80+
.join(', ');
81+
} catch (error: unknown) {
82+
const errorMessage =
83+
error instanceof Error ? error.message : String(error);
84+
// Only log errors to stderr (not stdout which MCP uses)
85+
console.error('Error serializing args:', errorMessage);
86+
argsDescription = `[args serialization failed: ${errorMessage}]`;
87+
}
88+
89+
const instruction = argsDescription
90+
? `Use the action "${action.name}" with ${argsDescription}`
91+
: `Use the action "${action.name}"`;
92+
93+
try {
94+
await agent.aiAction(instruction);
95+
} catch (error: unknown) {
96+
const errorMessage =
97+
error instanceof Error ? error.message : String(error);
98+
console.error(
99+
`Error executing action "${action.name}":`,
100+
errorMessage,
101+
);
102+
return {
103+
content: [
104+
{
105+
type: 'text',
106+
text: `Failed to execute action "${action.name}": ${errorMessage}`,
107+
},
108+
],
109+
isError: true,
110+
};
111+
}
112+
}
46113

47-
// Return screenshot after action
48-
const screenshot = await agent.page?.screenshotBase64();
49-
if (!screenshot) {
114+
// Return screenshot after action
115+
try {
116+
const screenshot = await agent.page?.screenshotBase64();
117+
if (!screenshot) {
118+
return {
119+
content: [
120+
{
121+
type: 'text',
122+
text: `Action "${action.name}" completed.`,
123+
},
124+
],
125+
};
126+
}
127+
128+
const { mimeType, body } = parseBase64(screenshot);
129+
130+
return {
131+
content: [
132+
{
133+
type: 'text',
134+
text: `Action "${action.name}" completed.`,
135+
},
136+
{
137+
type: 'image',
138+
data: body,
139+
mimeType,
140+
},
141+
],
142+
};
143+
} catch (error: unknown) {
144+
const errorMessage =
145+
error instanceof Error ? error.message : String(error);
146+
console.error('Error capturing screenshot:', errorMessage);
147+
// Action completed but screenshot failed - still return success
148+
return {
149+
content: [
150+
{
151+
type: 'text',
152+
text: `Action "${action.name}" completed (screenshot unavailable: ${errorMessage})`,
153+
},
154+
],
155+
};
156+
}
157+
} catch (error: unknown) {
158+
const errorMessage =
159+
error instanceof Error ? error.message : String(error);
160+
console.error(`Error in handler for "${action.name}":`, errorMessage);
50161
return {
51162
content: [
52163
{
53164
type: 'text',
54-
text: `Action "${action.name}" completed.`,
165+
text: `Failed to get agent or execute action "${action.name}": ${errorMessage}`,
55166
},
56167
],
168+
isError: true,
57169
};
58170
}
59-
60-
const { mimeType, body } = parseBase64(screenshot);
61-
62-
return {
63-
content: [
64-
{
65-
type: 'text',
66-
text: `Action "${action.name}" completed.`,
67-
},
68-
{
69-
type: 'image',
70-
data: body,
71-
mimeType,
72-
},
73-
],
74-
};
75171
},
76172
autoDestroy: true,
77173
};
@@ -91,18 +187,33 @@ export function generateCommonTools(
91187
description: 'Capture screenshot of current page/screen',
92188
schema: {},
93189
handler: async () => {
94-
const agent = await getAgent();
95-
const screenshot = await agent.page?.screenshotBase64();
96-
if (!screenshot) {
190+
try {
191+
const agent = await getAgent();
192+
const screenshot = await agent.page?.screenshotBase64();
193+
if (!screenshot) {
194+
return {
195+
content: [{ type: 'text', text: 'Screenshot not available' }],
196+
isError: true,
197+
};
198+
}
199+
const { mimeType, body } = parseBase64(screenshot);
200+
return {
201+
content: [{ type: 'image', data: body, mimeType }],
202+
};
203+
} catch (error: unknown) {
204+
const errorMessage =
205+
error instanceof Error ? error.message : String(error);
206+
console.error('Error taking screenshot:', errorMessage);
97207
return {
98-
content: [{ type: 'text', text: 'Screenshot not available' }],
208+
content: [
209+
{
210+
type: 'text',
211+
text: `Failed to capture screenshot: ${errorMessage}`,
212+
},
213+
],
99214
isError: true,
100215
};
101216
}
102-
const { mimeType, body } = parseBase64(screenshot);
103-
return {
104-
content: [{ type: 'image', data: body, mimeType }],
105-
};
106217
},
107218
autoDestroy: true,
108219
},
@@ -115,21 +226,36 @@ export function generateCommonTools(
115226
checkIntervalMs: z.number().optional().default(3000),
116227
},
117228
handler: async (args) => {
118-
const agent = await getAgent();
119-
const { assertion, timeoutMs, checkIntervalMs } = args as {
120-
assertion: string;
121-
timeoutMs?: number;
122-
checkIntervalMs?: number;
123-
};
229+
try {
230+
const agent = await getAgent();
231+
const { assertion, timeoutMs, checkIntervalMs } = args as {
232+
assertion: string;
233+
timeoutMs?: number;
234+
checkIntervalMs?: number;
235+
};
124236

125-
if (agent.aiWaitFor) {
126-
await agent.aiWaitFor(assertion, { timeoutMs, checkIntervalMs });
127-
}
237+
if (agent.aiWaitFor) {
238+
await agent.aiWaitFor(assertion, { timeoutMs, checkIntervalMs });
239+
}
128240

129-
return {
130-
content: [{ type: 'text', text: `Condition met: "${assertion}"` }],
131-
isError: false,
132-
};
241+
return {
242+
content: [{ type: 'text', text: `Condition met: "${assertion}"` }],
243+
isError: false,
244+
};
245+
} catch (error: unknown) {
246+
const errorMessage =
247+
error instanceof Error ? error.message : String(error);
248+
console.error('Error in wait_for:', errorMessage);
249+
return {
250+
content: [
251+
{
252+
type: 'text',
253+
text: `Wait condition failed: ${errorMessage}`,
254+
},
255+
],
256+
isError: true,
257+
};
258+
}
133259
},
134260
autoDestroy: true,
135261
},

packages/shared/src/mcp/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export interface BaseAgent {
8080
};
8181
aiAction?: (
8282
description: string,
83-
params: Record<string, unknown>,
83+
params?: Record<string, unknown>,
8484
) => Promise<void>;
8585
aiWaitFor?: (
8686
assertion: string,

0 commit comments

Comments
 (0)