Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions packages/shared/src/mcp/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import express, {
type Request,
type Response,
} from 'express';
import { getErrorMessage } from './error-formatter';
import type { IMidsceneTools } from './types';

export interface BaseMCPServerConfig {
Expand Down Expand Up @@ -119,7 +120,7 @@ export abstract class BaseMCPServer {
try {
await this.toolsManager.initTools();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(`Failed to initialize tools: ${message}`);
console.error('Tools will be initialized on first use');
}
Expand Down Expand Up @@ -160,7 +161,7 @@ export abstract class BaseMCPServer {
try {
await this.mcpServer.connect(transport);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(`Failed to connect MCP stdio transport: ${message}`);
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
}
Expand Down Expand Up @@ -282,7 +283,7 @@ export abstract class BaseMCPServer {
.json({ error: 'Invalid session or GET without session' });
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
const duration = Date.now() - startTime;
console.error(
`[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
Expand Down Expand Up @@ -336,8 +337,7 @@ export abstract class BaseMCPServer {
try {
await session.transport.close();
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(
`Failed to close session ${session.transport.sessionId}: ${message}`,
);
Expand Down Expand Up @@ -390,7 +390,7 @@ export abstract class BaseMCPServer {
try {
await this.mcpServer.connect(transport);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(
`[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
);
Expand Down Expand Up @@ -425,8 +425,7 @@ export abstract class BaseMCPServer {
`[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
);
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(
`[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
);
Expand Down Expand Up @@ -455,8 +454,7 @@ export abstract class BaseMCPServer {
try {
session.transport.close();
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(`Error closing session during shutdown: ${message}`);
}
}
Expand All @@ -475,7 +473,7 @@ export abstract class BaseMCPServer {
this.performCleanup().finally(() => process.exit(1));
}, 5000);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const message = getErrorMessage(error);
console.error(`Error closing HTTP server: ${message}`);
this.performCleanup().finally(() => process.exit(1));
}
Expand Down
52 changes: 52 additions & 0 deletions packages/shared/src/mcp/error-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Extract a human-readable message from an unknown thrown value.
*
* Many SDK/transport layers reject with structured objects (e.g.
* `{ code, message }`, `{ error: { message } }`, `{ cause: { message } }`)
* rather than `Error` instances. `String(obj)` collapses those to
* `"[object Object]"`, which is useless for diagnostics. This helper walks
* the common shapes, falls back to `JSON.stringify`, and finally to
* `Object.prototype.toString.call` so that callers always get something
* actionable in logs and surfaced tool results.
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (error === null || error === undefined) return String(error);
if (typeof error !== 'object') return String(error);

const candidate = extractStringMessage(error);
if (candidate) return candidate;

try {
return JSON.stringify(error);
} catch {
return Object.prototype.toString.call(error);
}
}

function extractStringMessage(error: object): string | undefined {
const anyError = error as {
message?: unknown;
error?: { message?: unknown };
cause?: { message?: unknown };
};

if (typeof anyError.message === 'string' && anyError.message) {
return anyError.message;
}
if (
anyError.error &&
typeof anyError.error.message === 'string' &&
anyError.error.message
) {
return anyError.error.message;
}
if (
anyError.cause &&
typeof anyError.cause.message === 'string' &&
anyError.cause.message
) {
return anyError.cause.message;
}
return undefined;
}
1 change: 1 addition & 0 deletions packages/shared/src/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './base-server';
export * from './base-tools';
export * from './error-formatter';
export * from './tool-generator';
export * from './types';
export * from './inject-report-html-plugin';
Expand Down
8 changes: 1 addition & 7 deletions packages/shared/src/mcp/tool-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@ import {
isMidsceneLocatorField,
unwrapZodField,
} from '../zod-schema-utils';
import { getErrorMessage } from './error-formatter';
import type {
ActionSpaceItem,
BaseAgent,
ToolDefinition,
ToolResult,
} from './types';

/**
* Extract error message from unknown error type
*/
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

/**
* Generate MCP tool description from ActionSpaceItem
* Format: "actionName action, description. Parameters: param1 (type) - desc; param2 (type) - desc"
Expand Down
76 changes: 76 additions & 0 deletions packages/shared/tests/unit-test/error-formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getErrorMessage } from '@/mcp/error-formatter';
import { describe, expect, it } from 'vitest';

describe('getErrorMessage', () => {
it('returns the Error.message for Error instances', () => {
expect(getErrorMessage(new Error('boom'))).toBe('boom');
expect(getErrorMessage(new TypeError('bad type'))).toBe('bad type');
});

it('stringifies null and undefined', () => {
expect(getErrorMessage(null)).toBe('null');
expect(getErrorMessage(undefined)).toBe('undefined');
});

it('stringifies primitives', () => {
expect(getErrorMessage('oops')).toBe('oops');
expect(getErrorMessage(42)).toBe('42');
expect(getErrorMessage(true)).toBe('true');
});

it('extracts message from { message } shape', () => {
expect(getErrorMessage({ message: 'connect ECONNREFUSED' })).toBe(
'connect ECONNREFUSED',
);
});

it('extracts message from { error: { message } } shape', () => {
expect(
getErrorMessage({ error: { message: 'upstream failed', code: 502 } }),
).toBe('upstream failed');
});

it('extracts message from { cause: { message } } shape', () => {
expect(getErrorMessage({ cause: { message: 'root cause' } })).toBe(
'root cause',
);
});

it('prefers top-level message over nested error/cause', () => {
expect(
getErrorMessage({
message: 'outer',
error: { message: 'inner' },
cause: { message: 'root' },
}),
).toBe('outer');
});

it('skips empty string messages and falls through to JSON', () => {
expect(getErrorMessage({ message: '', code: 'EIO' })).toBe(
'{"message":"","code":"EIO"}',
);
});

it('serializes plain objects without a known message field', () => {
expect(getErrorMessage({ status: 500, details: 'x' })).toBe(
'{"status":500,"details":"x"}',
);
});

it('falls back to Object.prototype.toString for unserializable objects', () => {
const circular: Record<string, unknown> = { foo: 'bar' };
circular.self = circular;
expect(getErrorMessage(circular)).toBe('[object Object]');
});

it('never returns the literal "[object Object]" for plain objects with data', () => {
const result = getErrorMessage({ code: 'E_TEST', detail: 'something' });
expect(result).not.toBe('[object Object]');
expect(result).toContain('E_TEST');
});

it('handles arrays by JSON-stringifying them', () => {
expect(getErrorMessage([1, 2, 3])).toBe('[1,2,3]');
});
});
5 changes: 5 additions & 0 deletions packages/web-integration/src/cdp-proxy-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export const PROXY_ENDPOINT_FILE = join(
'midscene-cdp-proxy-endpoint',
);
export const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');
export const PROXY_UPSTREAM_FILE = join(
tmpdir(),
'midscene-cdp-proxy-upstream',
);
export const TARGET_ID_FILE = join(tmpdir(), 'midscene-cdp-target-id');
Loading
Loading