Skip to content

Commit 82bffa2

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

File tree

4 files changed

+198
-51
lines changed

4 files changed

+198
-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: 177 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,31 @@ 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>)
26+
.shape;
27+
28+
// Deep clone and modify the shape to make locate.prompt optional
29+
schema = Object.fromEntries(
30+
Object.entries(originalShape).map(([key, value]) => {
31+
// Check if this is a locate field (contains prompt field)
32+
if (
33+
value &&
34+
typeof value === 'object' &&
35+
'_def' in value &&
36+
(value as any)._def?.typeName === 'ZodObject' &&
37+
'shape' in value
38+
) {
39+
const fieldShape = (value as any).shape;
40+
if ('prompt' in fieldShape) {
41+
// This is a locate field, make prompt optional
42+
const newFieldShape = { ...fieldShape };
43+
newFieldShape.prompt = fieldShape.prompt.optional();
44+
return [key, z.object(newFieldShape).passthrough()];
45+
}
46+
}
47+
return [key, value];
48+
}),
49+
);
2650
} else {
2751
// Otherwise use it as-is
2852
schema = paramSchema as unknown as Record<string, z.ZodTypeAny>;
@@ -34,44 +58,117 @@ export function generateToolsFromActionSpace(
3458
description: action.description || `Execute ${action.name} action`,
3559
schema,
3660
handler: async (args: Record<string, unknown>) => {
37-
const agent = await getAgent();
61+
try {
62+
const agent = await getAgent();
3863

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

47-
// Return screenshot after action
48-
const screenshot = await agent.page?.screenshotBase64();
49-
if (!screenshot) {
115+
// Return screenshot after action
116+
try {
117+
const screenshot = await agent.page?.screenshotBase64();
118+
if (!screenshot) {
119+
return {
120+
content: [
121+
{
122+
type: 'text',
123+
text: `Action "${action.name}" completed.`,
124+
},
125+
],
126+
};
127+
}
128+
129+
const { mimeType, body } = parseBase64(screenshot);
130+
131+
return {
132+
content: [
133+
{
134+
type: 'text',
135+
text: `Action "${action.name}" completed.`,
136+
},
137+
{
138+
type: 'image',
139+
data: body,
140+
mimeType,
141+
},
142+
],
143+
};
144+
} catch (error: unknown) {
145+
const errorMessage =
146+
error instanceof Error ? error.message : String(error);
147+
console.error('Error capturing screenshot:', errorMessage);
148+
// Action completed but screenshot failed - still return success
149+
return {
150+
content: [
151+
{
152+
type: 'text',
153+
text: `Action "${action.name}" completed (screenshot unavailable: ${errorMessage})`,
154+
},
155+
],
156+
};
157+
}
158+
} catch (error: unknown) {
159+
const errorMessage =
160+
error instanceof Error ? error.message : String(error);
161+
console.error(`Error in handler for "${action.name}":`, errorMessage);
50162
return {
51163
content: [
52164
{
53165
type: 'text',
54-
text: `Action "${action.name}" completed.`,
166+
text: `Failed to get agent or execute action "${action.name}": ${errorMessage}`,
55167
},
56168
],
169+
isError: true,
57170
};
58171
}
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-
};
75172
},
76173
autoDestroy: true,
77174
};
@@ -91,18 +188,33 @@ export function generateCommonTools(
91188
description: 'Capture screenshot of current page/screen',
92189
schema: {},
93190
handler: async () => {
94-
const agent = await getAgent();
95-
const screenshot = await agent.page?.screenshotBase64();
96-
if (!screenshot) {
191+
try {
192+
const agent = await getAgent();
193+
const screenshot = await agent.page?.screenshotBase64();
194+
if (!screenshot) {
195+
return {
196+
content: [{ type: 'text', text: 'Screenshot not available' }],
197+
isError: true,
198+
};
199+
}
200+
const { mimeType, body } = parseBase64(screenshot);
201+
return {
202+
content: [{ type: 'image', data: body, mimeType }],
203+
};
204+
} catch (error: unknown) {
205+
const errorMessage =
206+
error instanceof Error ? error.message : String(error);
207+
console.error('Error taking screenshot:', errorMessage);
97208
return {
98-
content: [{ type: 'text', text: 'Screenshot not available' }],
209+
content: [
210+
{
211+
type: 'text',
212+
text: `Failed to capture screenshot: ${errorMessage}`,
213+
},
214+
],
99215
isError: true,
100216
};
101217
}
102-
const { mimeType, body } = parseBase64(screenshot);
103-
return {
104-
content: [{ type: 'image', data: body, mimeType }],
105-
};
106218
},
107219
autoDestroy: true,
108220
},
@@ -115,21 +227,36 @@ export function generateCommonTools(
115227
checkIntervalMs: z.number().optional().default(3000),
116228
},
117229
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-
};
230+
try {
231+
const agent = await getAgent();
232+
const { assertion, timeoutMs, checkIntervalMs } = args as {
233+
assertion: string;
234+
timeoutMs?: number;
235+
checkIntervalMs?: number;
236+
};
124237

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

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

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)