Skip to content

Commit 6d8645d

Browse files
committed
adding more mcp tests
1 parent c185b65 commit 6d8645d

4 files changed

Lines changed: 114 additions & 16 deletions

File tree

src/core/agent.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
import { WorkspaceFileCollector } from './agent/WorkspaceFileCollector.js';
9797
import { ProviderConfigManager } from './agent/ProviderConfigManager.js';
9898
import { AutoReportManager } from '../reporting/AutoReportManager.js';
99+
import { isLikelyFilePathSlashInput } from './slashInputDetection.js';
99100

100101
export class AutohandAgent {
101102
private mentionContexts: { path: string; contents: string }[] = [];
@@ -1212,20 +1213,7 @@ If lint or tests fail, report the issues but do NOT commit.`;
12121213
return null;
12131214
}
12141215

1215-
// Check if it looks like a file path rather than a slash command
1216-
// Slash commands are short: /help, /model, /new
1217-
// File paths have: multiple /, /Users/, extensions like .png, .ts, etc.
1218-
const looksLikeFilePath = (text: string): boolean => {
1219-
// Has multiple path separators (e.g., /Users/foo/bar)
1220-
if ((text.match(/\//g) || []).length > 1) return true;
1221-
// Starts with common path prefixes
1222-
if (/^\/(?:Users|home|tmp|var|opt|etc|usr)\//i.test(text)) return true;
1223-
// Has a file extension
1224-
if (/\.[a-z0-9]{1,5}(?:\s|$)/i.test(text)) return true;
1225-
return false;
1226-
};
1227-
1228-
if (normalized.startsWith('/') && !looksLikeFilePath(normalized)) {
1216+
if (normalized.startsWith('/') && !isLikelyFilePathSlashInput(normalized)) {
12291217
// Parse command and arguments from input
12301218
const parts = normalized.split(/\s+/);
12311219
let command = parts[0];

src/core/slashInputDetection.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Autohand AI LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* Heuristic to distinguish slash commands from file paths that begin with "/".
9+
* Only inspects the first token so slash-command arguments (e.g. npm package
10+
* specs like "@playwright/mcp@latest") do not accidentally trigger path mode.
11+
*/
12+
export function isLikelyFilePathSlashInput(text: string): boolean {
13+
const trimmed = text.trim();
14+
if (!trimmed.startsWith('/')) {
15+
return false;
16+
}
17+
18+
const firstToken = trimmed.split(/\s+/, 1)[0] ?? '';
19+
20+
// Has nested path separators (e.g. /Users/foo/bar, /tmp/x)
21+
if ((firstToken.match(/\//g) || []).length > 1) {
22+
return true;
23+
}
24+
25+
// Starts with common absolute path prefixes
26+
if (/^\/(?:Users|home|tmp|var|opt|etc|usr)\//i.test(firstToken)) {
27+
return true;
28+
}
29+
30+
// Looks like a file with extension
31+
if (/\.[a-z0-9]{1,5}$/i.test(firstToken)) {
32+
return true;
33+
}
34+
35+
return false;
36+
}
37+

src/mcp/McpClientManager.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ class McpStdioConnection extends EventEmitter {
297297
while (this.frameBuffer.length > 0) {
298298
const header = this.findHeaderEnd(this.frameBuffer);
299299
if (!header) {
300+
// Compatibility: some legacy MCP servers speak newline-delimited JSON-RPC.
301+
// If buffered stdout looks like JSON lines, parse it immediately instead of
302+
// waiting for a Content-Length timeout.
303+
const preview = this.frameBuffer.toString('utf8', 0, Math.min(this.frameBuffer.length, 256));
304+
const trimmed = preview.trimStart();
305+
const looksLikeJsonLine = trimmed.startsWith('{') || trimmed.startsWith('[');
306+
if (looksLikeJsonLine && this.frameBuffer.includes(0x0a)) {
307+
this.parseLineDelimitedData(this.frameBuffer);
308+
this.frameBuffer = Buffer.alloc(0);
309+
}
300310
return;
301311
}
302312

@@ -817,7 +827,7 @@ export class McpClientManager {
817827
*/
818828
private async connectStdio(config: McpServerConfig): Promise<void> {
819829
try {
820-
const connected = await this.connectStdioWithFraming(config, 'content-length');
830+
const connected = await this.connectStdioWithFallbackFraming(config);
821831
this.registerConnectedStdioServer(config, connected.connection, connected.tools);
822832
} catch (error) {
823833
if (!this.shouldRetryNpxWithIsolatedCache(config, error)) {
@@ -830,7 +840,7 @@ export class McpClientManager {
830840
};
831841

832842
try {
833-
const connected = await this.connectStdioWithFraming(retryConfig, 'content-length');
843+
const connected = await this.connectStdioWithFallbackFraming(retryConfig);
834844
// Keep persisted config intact; retry cache env is only a runtime override.
835845
this.registerConnectedStdioServer(config, connected.connection, connected.tools);
836846
} catch (retryError) {
@@ -849,6 +859,42 @@ export class McpClientManager {
849859
return isRetriableNpxInstallError(message);
850860
}
851861

862+
/**
863+
* Some MCP servers still use newline-delimited JSON-RPC over stdio.
864+
* Start with Content-Length framing (spec), then fallback to newline when
865+
* initialize stalls/closes without a successful handshake.
866+
*/
867+
private shouldRetryWithNewlineFraming(error: unknown): boolean {
868+
const message = error instanceof Error ? error.message : String(error);
869+
return (
870+
message.includes('MCP request "initialize" timed out')
871+
|| message.includes('MCP connection closed before initialization completed')
872+
|| message.includes('MCP connection closed (server exited with code')
873+
);
874+
}
875+
876+
private async connectStdioWithFallbackFraming(
877+
config: McpServerConfig
878+
): Promise<{ connection: McpStdioConnection; tools: McpToolDefinition[] }> {
879+
try {
880+
return await this.connectStdioWithFraming(config, 'content-length');
881+
} catch (contentLengthError) {
882+
if (!this.shouldRetryWithNewlineFraming(contentLengthError)) {
883+
throw contentLengthError;
884+
}
885+
886+
try {
887+
return await this.connectStdioWithFraming(config, 'newline');
888+
} catch (newlineError) {
889+
const first = contentLengthError instanceof Error
890+
? contentLengthError.message
891+
: String(contentLengthError);
892+
const second = newlineError instanceof Error ? newlineError.message : String(newlineError);
893+
throw new Error(`${first}\nRetry with newline framing failed: ${second}`);
894+
}
895+
}
896+
}
897+
852898
/**
853899
* Tries to establish a stdio MCP connection with a specific framing mode.
854900
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Autohand AI LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import { describe, expect, it } from 'vitest';
7+
import { isLikelyFilePathSlashInput } from '../../src/core/slashInputDetection.js';
8+
9+
describe('isLikelyFilePathSlashInput', () => {
10+
it('detects absolute file paths', () => {
11+
expect(isLikelyFilePathSlashInput('/Users/me/project/file.ts')).toBe(true);
12+
expect(isLikelyFilePathSlashInput('/tmp/test.txt')).toBe(true);
13+
expect(isLikelyFilePathSlashInput('/opt/bin/script.sh --flag')).toBe(true);
14+
});
15+
16+
it('does not classify slash commands as file paths', () => {
17+
expect(isLikelyFilePathSlashInput('/mcp')).toBe(false);
18+
expect(isLikelyFilePathSlashInput('/mcp add playwright npx @playwright/mcp@latest')).toBe(false);
19+
expect(isLikelyFilePathSlashInput('/mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest')).toBe(false);
20+
});
21+
22+
it('ignores non-slash input', () => {
23+
expect(isLikelyFilePathSlashInput('mcp add')).toBe(false);
24+
expect(isLikelyFilePathSlashInput('@playwright/mcp@latest')).toBe(false);
25+
});
26+
});
27+

0 commit comments

Comments
 (0)