Compare commits

..

5 Commits

Author SHA1 Message Date
Spencer e9ba38ed5c Merge branch 'main' into debug-fix 2026-04-24 14:18:12 -04:00
Spencer 3f82b001d2 Merge branch 'main' into debug-fix 2026-04-24 13:10:03 -04:00
Spencer 2eebc2980b Merge branch 'main' into debug-fix 2026-04-23 19:24:09 -04:00
Spencer e96eb82a13 Merge branch 'main' into debug-fix 2026-04-23 19:03:32 -04:00
Spencer 85f99d174f fix(core): drain stderr stream unconditionally for StdioClientTransport 2026-04-23 22:51:17 +00:00
6 changed files with 58 additions and 59 deletions
+9 -5
View File
@@ -358,8 +358,8 @@ export async function main() {
const isDebugMode = cliConfig.isDebugMode(argv);
const consolePatcher = new ConsolePatcher({
stderr: argv.isCommand ? false : true,
interactive: isHeadlessMode() && !argv.isCommand ? false : true,
stderr: true,
interactive: isHeadlessMode() ? false : true,
debugMode: isDebugMode,
onNewMessage: (msg) => {
coreEvents.emitConsoleLog(msg.type, msg.content);
@@ -786,16 +786,20 @@ export function initializeOutputListenersAndFlush() {
if (coreEvents.listenerCount(CoreEvent.ConsoleLog) === 0) {
coreEvents.on(CoreEvent.ConsoleLog, (payload: ConsoleLogPayload) => {
if (payload.type === 'error' || payload.type === 'warn') {
writeToStderr(payload.content + '\n');
writeToStderr(payload.content);
} else {
writeToStderr(payload.content + '\n');
writeToStdout(payload.content);
}
});
}
if (coreEvents.listenerCount(CoreEvent.UserFeedback) === 0) {
coreEvents.on(CoreEvent.UserFeedback, (payload: UserFeedbackPayload) => {
writeToStderr(payload.message + '\n');
if (payload.severity === 'error' || payload.severity === 'warning') {
writeToStderr(payload.message);
} else {
writeToStdout(payload.message);
}
});
}
}
@@ -202,30 +202,6 @@ describe('McpStatus', () => {
unmount();
});
it('filters blocked servers case-insensitively and ignores whitespace', async () => {
const { lastFrame, unmount } = await render(
<McpStatus
{...baseProps}
servers={{
'SERVER-1 ': {
url: 'http://localhost:8080',
description: 'A test server',
},
' server-2': {
url: 'http://localhost:8081',
description: 'A blocked server',
},
}}
blockedServers={[
{ name: ' server-1', extensionName: 'test-extension' },
{ name: 'SERVER-2 ', extensionName: 'test-extension' },
]}
/>,
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders only blocked servers when no configured servers exist', async () => {
const { lastFrame, unmount } = await render(
<McpStatus
@@ -50,9 +50,7 @@ export const McpStatus: React.FC<McpStatusProps> = ({
const serverNames = Object.keys(servers).filter(
(serverName) =>
!blockedServers.some(
(blockedServer) =>
blockedServer.name.trim().toLowerCase() ===
serverName.trim().toLowerCase(),
(blockedServer) => blockedServer.name === serverName,
),
);
@@ -1,14 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`McpStatus > filters blocked servers case-insensitively and ignores whitespace 1`] = `
"Configured MCP servers:
🔴 server-1 (from test-extension) - Blocked
🔴 SERVER-2 (from test-extension) - Blocked
"
`;
exports[`McpStatus > renders correctly when discovery is in progress 1`] = `
"⏳ MCP servers are starting up (0 initializing)...
Note: First startup may take longer. Tool availability will update automatically.
@@ -1993,6 +1993,27 @@ describe('mcp-client', () => {
});
});
it('should unconditionally attach a data listener to stderr to prevent process hang', async () => {
const mockStderr = {
on: vi.fn(),
};
vi.spyOn(SdkClientStdioLib, 'StdioClientTransport').mockReturnValue({
stderr: mockStderr,
} as unknown as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
},
false, // debugMode = false
MOCK_CONTEXT,
);
expect(mockStderr.on).toHaveBeenCalledWith('data', expect.any(Function));
});
it('sets an env variable GEMINI_CLI=1 for stdio MCP servers', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
+27 -18
View File
@@ -2291,31 +2291,40 @@ export async function createTransport(
transport = new XcodeMcpBridgeFixTransport(transport);
}
if (debugMode) {
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const underlyingTransport =
transport instanceof XcodeMcpBridgeFixTransport
? // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(transport as any).transport
: transport;
const underlyingTransport =
transport instanceof XcodeMcpBridgeFixTransport
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(transport as unknown as { transport: unknown }).transport
: transport;
if (
underlyingTransport instanceof StdioClientTransport &&
underlyingTransport.stderr
) {
underlyingTransport.stderr.on('data', (data) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const stderrStr = data.toString().trim();
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const streamTransport = underlyingTransport as {
stderr?: {
on: (event: string, listener: (data: unknown) => void) => void;
};
};
if (
streamTransport &&
typeof streamTransport === 'object' &&
'stderr' in streamTransport &&
streamTransport.stderr &&
typeof streamTransport.stderr.on === 'function'
) {
streamTransport.stderr.on('data', (data: unknown) => {
if (debugMode) {
const stderrStr = String(data).trim();
debugLogger.debug(
`[DEBUG] [MCP STDERR (${mcpServerName})]: `,
stderrStr,
);
});
}
}
});
}
return transport;
}