style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,54 +1,41 @@
import React, {
createContext,
type ReactNode,
useContext,
useMemo,
} from 'react'
import type { Command } from '../../commands.js'
import type { Tool } from '../../Tool.js'
import type {
MCPServerConnection,
ScopedMcpServerConfig,
ServerResource,
} from './types.js'
import { useManageMCPConnections } from './useManageMCPConnections.js'
import React, { createContext, type ReactNode, useContext, useMemo } from 'react';
import type { Command } from '../../commands.js';
import type { Tool } from '../../Tool.js';
import type { MCPServerConnection, ScopedMcpServerConfig, ServerResource } from './types.js';
import { useManageMCPConnections } from './useManageMCPConnections.js';
interface MCPConnectionContextValue {
reconnectMcpServer: (serverName: string) => Promise<{
client: MCPServerConnection
tools: Tool[]
commands: Command[]
resources?: ServerResource[]
}>
toggleMcpServer: (serverName: string) => Promise<void>
client: MCPServerConnection;
tools: Tool[];
commands: Command[];
resources?: ServerResource[];
}>;
toggleMcpServer: (serverName: string) => Promise<void>;
}
const MCPConnectionContext = createContext<MCPConnectionContextValue | null>(
null,
)
const MCPConnectionContext = createContext<MCPConnectionContextValue | null>(null);
export function useMcpReconnect() {
const context = useContext(MCPConnectionContext)
const context = useContext(MCPConnectionContext);
if (!context) {
throw new Error('useMcpReconnect must be used within MCPConnectionManager')
throw new Error('useMcpReconnect must be used within MCPConnectionManager');
}
return context.reconnectMcpServer
return context.reconnectMcpServer;
}
export function useMcpToggleEnabled() {
const context = useContext(MCPConnectionContext)
const context = useContext(MCPConnectionContext);
if (!context) {
throw new Error(
'useMcpToggleEnabled must be used within MCPConnectionManager',
)
throw new Error('useMcpToggleEnabled must be used within MCPConnectionManager');
}
return context.toggleMcpServer
return context.toggleMcpServer;
}
interface MCPConnectionManagerProps {
children: ReactNode
dynamicMcpConfig: Record<string, ScopedMcpServerConfig> | undefined
isStrictMcpConfig: boolean
children: ReactNode;
dynamicMcpConfig: Record<string, ScopedMcpServerConfig> | undefined;
isStrictMcpConfig: boolean;
}
// TODO (ollie): We may be able to get rid of this context by putting these function on app state
@@ -57,18 +44,8 @@ export function MCPConnectionManager({
dynamicMcpConfig,
isStrictMcpConfig,
}: MCPConnectionManagerProps): React.ReactNode {
const { reconnectMcpServer, toggleMcpServer } = useManageMCPConnections(
dynamicMcpConfig,
isStrictMcpConfig,
)
const value = useMemo(
() => ({ reconnectMcpServer, toggleMcpServer }),
[reconnectMcpServer, toggleMcpServer],
)
const { reconnectMcpServer, toggleMcpServer } = useManageMCPConnections(dynamicMcpConfig, isStrictMcpConfig);
const value = useMemo(() => ({ reconnectMcpServer, toggleMcpServer }), [reconnectMcpServer, toggleMcpServer]);
return (
<MCPConnectionContext.Provider value={value}>
{children}
</MCPConnectionContext.Provider>
)
return <MCPConnectionContext.Provider value={value}>{children}</MCPConnectionContext.Provider>;
}

View File

@@ -1,10 +1,10 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
// findChannelEntry extracted from ../channelNotification.ts (line 161)
// Copied to avoid heavy import chain
type ChannelEntry = {
kind: "server" | "plugin"
kind: 'server' | 'plugin'
name: string
}
@@ -12,58 +12,58 @@ function findChannelEntry(
serverName: string,
channels: readonly ChannelEntry[],
): ChannelEntry | undefined {
const parts = serverName.split(":")
const parts = serverName.split(':')
return channels.find(c =>
c.kind === "server"
c.kind === 'server'
? serverName === c.name
: parts[0] === "plugin" && parts[1] === c.name,
: parts[0] === 'plugin' && parts[1] === c.name,
)
}
describe("findChannelEntry", () => {
test("finds server entry by exact name match", () => {
const channels = [{ kind: "server" as const, name: "my-server" }]
expect(findChannelEntry("my-server", channels)).toBeDefined()
expect(findChannelEntry("my-server", channels)!.name).toBe("my-server")
describe('findChannelEntry', () => {
test('finds server entry by exact name match', () => {
const channels = [{ kind: 'server' as const, name: 'my-server' }]
expect(findChannelEntry('my-server', channels)).toBeDefined()
expect(findChannelEntry('my-server', channels)!.name).toBe('my-server')
})
test("finds plugin entry by matching second segment", () => {
const channels = [{ kind: "plugin" as const, name: "slack" }]
expect(findChannelEntry("plugin:slack:tg", channels)).toBeDefined()
test('finds plugin entry by matching second segment', () => {
const channels = [{ kind: 'plugin' as const, name: 'slack' }]
expect(findChannelEntry('plugin:slack:tg', channels)).toBeDefined()
})
test("returns undefined for no match", () => {
const channels = [{ kind: "server" as const, name: "other" }]
expect(findChannelEntry("my-server", channels)).toBeUndefined()
test('returns undefined for no match', () => {
const channels = [{ kind: 'server' as const, name: 'other' }]
expect(findChannelEntry('my-server', channels)).toBeUndefined()
})
test("handles empty channels array", () => {
expect(findChannelEntry("my-server", [])).toBeUndefined()
test('handles empty channels array', () => {
expect(findChannelEntry('my-server', [])).toBeUndefined()
})
test("handles server name without colon", () => {
const channels = [{ kind: "server" as const, name: "simple" }]
expect(findChannelEntry("simple", channels)).toBeDefined()
test('handles server name without colon', () => {
const channels = [{ kind: 'server' as const, name: 'simple' }]
expect(findChannelEntry('simple', channels)).toBeDefined()
})
test("handles 'plugin:name' format correctly", () => {
const channels = [{ kind: "plugin" as const, name: "slack" }]
expect(findChannelEntry("plugin:slack:tg", channels)).toBeDefined()
expect(findChannelEntry("plugin:discord:tg", channels)).toBeUndefined()
const channels = [{ kind: 'plugin' as const, name: 'slack' }]
expect(findChannelEntry('plugin:slack:tg', channels)).toBeDefined()
expect(findChannelEntry('plugin:discord:tg', channels)).toBeUndefined()
})
test("prefers exact match (server kind) over partial match", () => {
test('prefers exact match (server kind) over partial match', () => {
const channels = [
{ kind: "server" as const, name: "plugin:slack" },
{ kind: "plugin" as const, name: "slack" },
{ kind: 'server' as const, name: 'plugin:slack' },
{ kind: 'plugin' as const, name: 'slack' },
]
const result = findChannelEntry("plugin:slack", channels)
const result = findChannelEntry('plugin:slack', channels)
expect(result).toBeDefined()
expect(result!.kind).toBe("server")
expect(result!.kind).toBe('server')
})
test("plugin kind does not match bare name", () => {
const channels = [{ kind: "plugin" as const, name: "slack" }]
expect(findChannelEntry("slack", channels)).toBeUndefined()
test('plugin kind does not match bare name', () => {
const channels = [{ kind: 'plugin' as const, name: 'slack' }]
expect(findChannelEntry('slack', channels)).toBeUndefined()
})
})

View File

@@ -1,8 +1,8 @@
import { mock, describe, expect, test } from "bun:test";
import { mock, describe, expect, test } from 'bun:test'
mock.module("src/services/analytics/growthbook.js", () => ({
mock.module('src/services/analytics/growthbook.js', () => ({
getFeatureValue_CACHED_MAY_BE_STALE: () => false,
}));
}))
const {
filterPermissionRelayClients,
@@ -10,185 +10,187 @@ const {
truncateForPreview,
PERMISSION_REPLY_RE,
createChannelPermissionCallbacks,
} = await import("../channelPermissions");
} = await import('../channelPermissions')
describe("shortRequestId", () => {
test("returns 5-char string from tool use ID", () => {
const result = shortRequestId("toolu_abc123");
expect(result).toHaveLength(5);
});
describe('shortRequestId', () => {
test('returns 5-char string from tool use ID', () => {
const result = shortRequestId('toolu_abc123')
expect(result).toHaveLength(5)
})
test("is deterministic (same input = same output)", () => {
const a = shortRequestId("toolu_abc123");
const b = shortRequestId("toolu_abc123");
expect(a).toBe(b);
});
test('is deterministic (same input = same output)', () => {
const a = shortRequestId('toolu_abc123')
const b = shortRequestId('toolu_abc123')
expect(a).toBe(b)
})
test("different inputs produce different outputs", () => {
const a = shortRequestId("toolu_aaa");
const b = shortRequestId("toolu_bbb");
expect(a).not.toBe(b);
});
test('different inputs produce different outputs', () => {
const a = shortRequestId('toolu_aaa')
const b = shortRequestId('toolu_bbb')
expect(a).not.toBe(b)
})
test("result contains only valid letters (no 'l')", () => {
const validChars = new Set("abcdefghijkmnopqrstuvwxyz");
const validChars = new Set('abcdefghijkmnopqrstuvwxyz')
for (let i = 0; i < 50; i++) {
const result = shortRequestId(`toolu_${i}`);
const result = shortRequestId(`toolu_${i}`)
for (const ch of result) {
expect(validChars.has(ch)).toBe(true);
expect(validChars.has(ch)).toBe(true)
}
}
});
})
test("handles empty string", () => {
const result = shortRequestId("");
expect(result).toHaveLength(5);
});
});
test('handles empty string', () => {
const result = shortRequestId('')
expect(result).toHaveLength(5)
})
})
describe("truncateForPreview", () => {
test("returns JSON string for object input", () => {
const result = truncateForPreview({ key: "value" });
expect(result).toBe('{"key":"value"}');
});
describe('truncateForPreview', () => {
test('returns JSON string for object input', () => {
const result = truncateForPreview({ key: 'value' })
expect(result).toBe('{"key":"value"}')
})
test("truncates to <=200 chars with ellipsis when input is long", () => {
const longObj = { data: "x".repeat(300) };
const result = truncateForPreview(longObj);
expect(result.length).toBeLessThanOrEqual(203); // 200 + '…'
expect(result.endsWith("…")).toBe(true);
});
test('truncates to <=200 chars with ellipsis when input is long', () => {
const longObj = { data: 'x'.repeat(300) }
const result = truncateForPreview(longObj)
expect(result.length).toBeLessThanOrEqual(203) // 200 + '…'
expect(result.endsWith('…')).toBe(true)
})
test("returns short input unchanged", () => {
const result = truncateForPreview({ a: 1 });
expect(result).toBe('{"a":1}');
expect(result.endsWith("…")).toBe(false);
});
test('returns short input unchanged', () => {
const result = truncateForPreview({ a: 1 })
expect(result).toBe('{"a":1}')
expect(result.endsWith('…')).toBe(false)
})
test("handles string input", () => {
const result = truncateForPreview("hello");
expect(result).toBe('"hello"');
});
test('handles string input', () => {
const result = truncateForPreview('hello')
expect(result).toBe('"hello"')
})
test("handles null input", () => {
const result = truncateForPreview(null);
expect(result).toBe("null");
});
test('handles null input', () => {
const result = truncateForPreview(null)
expect(result).toBe('null')
})
test("handles undefined input", () => {
const result = truncateForPreview(undefined);
test('handles undefined input', () => {
const result = truncateForPreview(undefined)
// JSON.stringify(undefined) returns undefined, then .length throws → catch returns '(unserializable)'
expect(result).toBe("(unserializable)");
});
});
expect(result).toBe('(unserializable)')
})
})
describe("PERMISSION_REPLY_RE", () => {
describe('PERMISSION_REPLY_RE', () => {
test("matches 'y abcde'", () => {
expect(PERMISSION_REPLY_RE.test("y abcde")).toBe(true);
});
expect(PERMISSION_REPLY_RE.test('y abcde')).toBe(true)
})
test("matches 'yes abcde'", () => {
expect(PERMISSION_REPLY_RE.test("yes abcde")).toBe(true);
});
expect(PERMISSION_REPLY_RE.test('yes abcde')).toBe(true)
})
test("matches 'n abcde'", () => {
expect(PERMISSION_REPLY_RE.test("n abcde")).toBe(true);
});
expect(PERMISSION_REPLY_RE.test('n abcde')).toBe(true)
})
test("matches 'no abcde'", () => {
expect(PERMISSION_REPLY_RE.test("no abcde")).toBe(true);
});
expect(PERMISSION_REPLY_RE.test('no abcde')).toBe(true)
})
test("is case-insensitive", () => {
expect(PERMISSION_REPLY_RE.test("Y abcde")).toBe(true);
expect(PERMISSION_REPLY_RE.test("YES abcde")).toBe(true);
});
test('is case-insensitive', () => {
expect(PERMISSION_REPLY_RE.test('Y abcde')).toBe(true)
expect(PERMISSION_REPLY_RE.test('YES abcde')).toBe(true)
})
test("does not match without ID", () => {
expect(PERMISSION_REPLY_RE.test("yes")).toBe(false);
});
test('does not match without ID', () => {
expect(PERMISSION_REPLY_RE.test('yes')).toBe(false)
})
test("captures the ID from reply", () => {
const match = "y abcde".match(PERMISSION_REPLY_RE);
expect(match?.[2]).toBe("abcde");
});
});
test('captures the ID from reply', () => {
const match = 'y abcde'.match(PERMISSION_REPLY_RE)
expect(match?.[2]).toBe('abcde')
})
})
describe("createChannelPermissionCallbacks", () => {
test("resolve returns false for unknown request ID", () => {
const cb = createChannelPermissionCallbacks();
expect(cb.resolve("unknown-id", "allow", "server")).toBe(false);
});
describe('createChannelPermissionCallbacks', () => {
test('resolve returns false for unknown request ID', () => {
const cb = createChannelPermissionCallbacks()
expect(cb.resolve('unknown-id', 'allow', 'server')).toBe(false)
})
test("onResponse + resolve triggers handler", () => {
const cb = createChannelPermissionCallbacks();
let received: any = null;
cb.onResponse("test-id", (response) => {
received = response;
});
expect(cb.resolve("test-id", "allow", "test-server")).toBe(true);
test('onResponse + resolve triggers handler', () => {
const cb = createChannelPermissionCallbacks()
let received: any = null
cb.onResponse('test-id', response => {
received = response
})
expect(cb.resolve('test-id', 'allow', 'test-server')).toBe(true)
expect(received).toEqual({
behavior: "allow",
fromServer: "test-server",
});
});
behavior: 'allow',
fromServer: 'test-server',
})
})
test("onResponse unsubscribe prevents resolve", () => {
const cb = createChannelPermissionCallbacks();
let called = false;
const unsub = cb.onResponse("test-id", () => {
called = true;
});
unsub();
expect(cb.resolve("test-id", "allow", "server")).toBe(false);
expect(called).toBe(false);
});
test('onResponse unsubscribe prevents resolve', () => {
const cb = createChannelPermissionCallbacks()
let called = false
const unsub = cb.onResponse('test-id', () => {
called = true
})
unsub()
expect(cb.resolve('test-id', 'allow', 'server')).toBe(false)
expect(called).toBe(false)
})
test("duplicate resolve returns false (already consumed)", () => {
const cb = createChannelPermissionCallbacks();
cb.onResponse("test-id", () => {});
expect(cb.resolve("test-id", "allow", "server")).toBe(true);
expect(cb.resolve("test-id", "allow", "server")).toBe(false);
});
test('duplicate resolve returns false (already consumed)', () => {
const cb = createChannelPermissionCallbacks()
cb.onResponse('test-id', () => {})
expect(cb.resolve('test-id', 'allow', 'server')).toBe(true)
expect(cb.resolve('test-id', 'allow', 'server')).toBe(false)
})
test("is case-insensitive for request IDs", () => {
const cb = createChannelPermissionCallbacks();
let received: any = null;
cb.onResponse("ABC", (response) => {
received = response;
});
expect(cb.resolve("abc", "deny", "server")).toBe(true);
expect(received?.behavior).toBe("deny");
});
});
test('is case-insensitive for request IDs', () => {
const cb = createChannelPermissionCallbacks()
let received: any = null
cb.onResponse('ABC', response => {
received = response
})
expect(cb.resolve('abc', 'deny', 'server')).toBe(true)
expect(received?.behavior).toBe('deny')
})
})
describe("filterPermissionRelayClients", () => {
test("requires truthy permission capability", () => {
describe('filterPermissionRelayClients', () => {
test('requires truthy permission capability', () => {
const clients = [
{
type: "connected",
name: "plugin:weixin:weixin",
type: 'connected',
name: 'plugin:weixin:weixin',
capabilities: {
experimental: {
"claude/channel": {},
"claude/channel/permission": false,
'claude/channel': {},
'claude/channel/permission': false,
},
},
},
{
type: "connected",
name: "plugin:telegram:telegram",
type: 'connected',
name: 'plugin:telegram:telegram',
capabilities: {
experimental: {
"claude/channel": {},
"claude/channel/permission": {},
'claude/channel': {},
'claude/channel/permission': {},
},
},
},
];
]
expect(
filterPermissionRelayClients(clients, () => true).map(client => client.name),
).toEqual(["plugin:telegram:telegram"]);
});
});
filterPermissionRelayClients(clients, () => true).map(
client => client.name,
),
).toEqual(['plugin:telegram:telegram'])
})
})

View File

@@ -1,148 +1,148 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { expandEnvVarsInString } from "../envExpansion";
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
import { expandEnvVarsInString } from '../envExpansion'
const ENV_OPEN = "$" + "{";
const ENV_CLOSE = "}";
const envExpr = (value: string): string => `${ENV_OPEN}${value}${ENV_CLOSE}`;
const ENV_OPEN = '$' + '{'
const ENV_CLOSE = '}'
const envExpr = (value: string): string => `${ENV_OPEN}${value}${ENV_CLOSE}`
describe("expandEnvVarsInString", () => {
describe('expandEnvVarsInString', () => {
// Save and restore env vars touched by tests
const savedEnv: Record<string, string | undefined> = {};
const savedEnv: Record<string, string | undefined> = {}
const trackedKeys = [
"TEST_HOME",
"MISSING",
"TEST_A",
"TEST_B",
"TEST_EMPTY",
"TEST_X",
"VAR",
"TEST_FOUND",
];
'TEST_HOME',
'MISSING',
'TEST_A',
'TEST_B',
'TEST_EMPTY',
'TEST_X',
'VAR',
'TEST_FOUND',
]
beforeEach(() => {
for (const key of trackedKeys) {
savedEnv[key] = process.env[key];
savedEnv[key] = process.env[key]
}
});
})
afterEach(() => {
for (const key of trackedKeys) {
if (savedEnv[key] === undefined) {
delete process.env[key];
delete process.env[key]
} else {
process.env[key] = savedEnv[key];
process.env[key] = savedEnv[key]
}
}
});
})
test("expands a single env var that exists", () => {
process.env.TEST_HOME = "/home/user";
const result = expandEnvVarsInString(envExpr("TEST_HOME"));
expect(result.expanded).toBe("/home/user");
expect(result.missingVars).toEqual([]);
});
test('expands a single env var that exists', () => {
process.env.TEST_HOME = '/home/user'
const result = expandEnvVarsInString(envExpr('TEST_HOME'))
expect(result.expanded).toBe('/home/user')
expect(result.missingVars).toEqual([])
})
test("returns original placeholder and tracks missing var when not found", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString(envExpr("MISSING"));
expect(result.expanded).toBe(envExpr("MISSING"));
expect(result.missingVars).toEqual(["MISSING"]);
});
test('returns original placeholder and tracks missing var when not found', () => {
delete process.env.MISSING
const result = expandEnvVarsInString(envExpr('MISSING'))
expect(result.expanded).toBe(envExpr('MISSING'))
expect(result.missingVars).toEqual(['MISSING'])
})
test("uses default value when var is missing and default is provided", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString(envExpr("MISSING:-fallback"));
expect(result.expanded).toBe("fallback");
expect(result.missingVars).toEqual([]);
});
test('uses default value when var is missing and default is provided', () => {
delete process.env.MISSING
const result = expandEnvVarsInString(envExpr('MISSING:-fallback'))
expect(result.expanded).toBe('fallback')
expect(result.missingVars).toEqual([])
})
test("expands multiple vars", () => {
process.env.TEST_A = "hello";
process.env.TEST_B = "world";
test('expands multiple vars', () => {
process.env.TEST_A = 'hello'
process.env.TEST_B = 'world'
const result = expandEnvVarsInString(
`${envExpr("TEST_A")}/${envExpr("TEST_B")}`,
);
expect(result.expanded).toBe("hello/world");
expect(result.missingVars).toEqual([]);
});
`${envExpr('TEST_A')}/${envExpr('TEST_B')}`,
)
expect(result.expanded).toBe('hello/world')
expect(result.missingVars).toEqual([])
})
test("handles mix of found and missing vars", () => {
process.env.TEST_FOUND = "yes";
delete process.env.MISSING;
test('handles mix of found and missing vars', () => {
process.env.TEST_FOUND = 'yes'
delete process.env.MISSING
const result = expandEnvVarsInString(
`${envExpr("TEST_FOUND")}-${envExpr("MISSING")}`,
);
expect(result.expanded).toBe(`yes-${envExpr("MISSING")}`);
expect(result.missingVars).toEqual(["MISSING"]);
});
`${envExpr('TEST_FOUND')}-${envExpr('MISSING')}`,
)
expect(result.expanded).toBe(`yes-${envExpr('MISSING')}`)
expect(result.missingVars).toEqual(['MISSING'])
})
test("returns plain string unchanged with empty missingVars", () => {
const result = expandEnvVarsInString("plain string");
expect(result.expanded).toBe("plain string");
expect(result.missingVars).toEqual([]);
});
test('returns plain string unchanged with empty missingVars', () => {
const result = expandEnvVarsInString('plain string')
expect(result.expanded).toBe('plain string')
expect(result.missingVars).toEqual([])
})
test("expands empty env var value", () => {
process.env.TEST_EMPTY = "";
const result = expandEnvVarsInString(envExpr("TEST_EMPTY"));
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test('expands empty env var value', () => {
process.env.TEST_EMPTY = ''
const result = expandEnvVarsInString(envExpr('TEST_EMPTY'))
expect(result.expanded).toBe('')
expect(result.missingVars).toEqual([])
})
test("prefers env var value over default when var exists", () => {
process.env.TEST_X = "real";
const result = expandEnvVarsInString(envExpr("TEST_X:-default"));
expect(result.expanded).toBe("real");
expect(result.missingVars).toEqual([]);
});
test('prefers env var value over default when var exists', () => {
process.env.TEST_X = 'real'
const result = expandEnvVarsInString(envExpr('TEST_X:-default'))
expect(result.expanded).toBe('real')
expect(result.missingVars).toEqual([])
})
test("handles default value containing colons", () => {
test('handles default value containing colons', () => {
// split(':-', 2) means only the first :- is the delimiter
delete process.env.TEST_X;
const result = expandEnvVarsInString(envExpr("TEST_X:-value:-with:-colons"));
delete process.env.TEST_X
const result = expandEnvVarsInString(envExpr('TEST_X:-value:-with:-colons'))
// The default is "value" because split(':-', 2) gives ["TEST_X", "value"]
// Wait -- actually split(':-', 2) on "TEST_X:-value:-with:-colons" gives:
// ["TEST_X", "value"] because limit=2 stops at 2 pieces
expect(result.expanded).toBe("value");
expect(result.missingVars).toEqual([]);
});
expect(result.expanded).toBe('value')
expect(result.missingVars).toEqual([])
})
test("handles nested-looking syntax as literal (not supported)", () => {
test('handles nested-looking syntax as literal (not supported)', () => {
// ${${VAR}} - the regex [^}]+ matches "${VAR" (up to first })
// so varName would be "${VAR" which won't be found in env
delete process.env.VAR;
const nestedExpr = `${ENV_OPEN}${envExpr("VAR")}${ENV_CLOSE}`;
const result = expandEnvVarsInString(nestedExpr);
delete process.env.VAR
const nestedExpr = `${ENV_OPEN}${envExpr('VAR')}${ENV_CLOSE}`
const result = expandEnvVarsInString(nestedExpr)
// The regex \$\{([^}]+)\} matches "${${VAR}" with capture "${VAR"
// That env var won't exist, so it stays as "${${VAR}" + remaining "}"
expect(result.missingVars).toEqual([`${ENV_OPEN}VAR`]);
expect(result.expanded).toBe(nestedExpr);
});
expect(result.missingVars).toEqual([`${ENV_OPEN}VAR`])
expect(result.expanded).toBe(nestedExpr)
})
test("handles empty string input", () => {
const result = expandEnvVarsInString("");
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test('handles empty string input', () => {
const result = expandEnvVarsInString('')
expect(result.expanded).toBe('')
expect(result.missingVars).toEqual([])
})
test("handles var surrounded by text", () => {
process.env.TEST_A = "middle";
const result = expandEnvVarsInString(`before-${envExpr("TEST_A")}-after`);
expect(result.expanded).toBe("before-middle-after");
expect(result.missingVars).toEqual([]);
});
test('handles var surrounded by text', () => {
process.env.TEST_A = 'middle'
const result = expandEnvVarsInString(`before-${envExpr('TEST_A')}-after`)
expect(result.expanded).toBe('before-middle-after')
expect(result.missingVars).toEqual([])
})
test("handles default value that is empty string", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString(envExpr("MISSING:-"));
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test('handles default value that is empty string', () => {
delete process.env.MISSING
const result = expandEnvVarsInString(envExpr('MISSING:-'))
expect(result.expanded).toBe('')
expect(result.missingVars).toEqual([])
})
test("does not expand $VAR without braces", () => {
process.env.TEST_A = "value";
const result = expandEnvVarsInString("$TEST_A");
expect(result.expanded).toBe("$TEST_A");
expect(result.missingVars).toEqual([]);
});
});
test('does not expand $VAR without braces', () => {
process.env.TEST_A = 'value'
const result = expandEnvVarsInString('$TEST_A')
expect(result.expanded).toBe('$TEST_A')
expect(result.missingVars).toEqual([])
})
})

View File

@@ -1,11 +1,11 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
// parseHeaders is a pure function from ../utils.ts (line 325)
// Copied here to avoid triggering the heavy import chain of utils.ts
function parseHeaders(headerArray: string[]): Record<string, string> {
const headers: Record<string, string> = {}
for (const header of headerArray) {
const colonIndex = header.indexOf(":")
const colonIndex = header.indexOf(':')
if (colonIndex === -1) {
throw new Error(
`Invalid header format: "${header}". Expected format: "Header-Name: value"`,
@@ -23,43 +23,43 @@ function parseHeaders(headerArray: string[]): Record<string, string> {
return headers
}
describe("parseHeaders", () => {
describe('parseHeaders', () => {
test("parses 'Key: Value' format", () => {
expect(parseHeaders(["Content-Type: application/json"])).toEqual({
"Content-Type": "application/json",
});
});
expect(parseHeaders(['Content-Type: application/json'])).toEqual({
'Content-Type': 'application/json',
})
})
test("parses multiple headers", () => {
expect(parseHeaders(["Key1: val1", "Key2: val2"])).toEqual({
Key1: "val1",
Key2: "val2",
});
});
test('parses multiple headers', () => {
expect(parseHeaders(['Key1: val1', 'Key2: val2'])).toEqual({
Key1: 'val1',
Key2: 'val2',
})
})
test("trims whitespace around key and value", () => {
expect(parseHeaders([" Key : Value "])).toEqual({ Key: "Value" });
});
test('trims whitespace around key and value', () => {
expect(parseHeaders([' Key : Value '])).toEqual({ Key: 'Value' })
})
test("throws on missing colon", () => {
expect(() => parseHeaders(["no colon here"])).toThrow();
});
test('throws on missing colon', () => {
expect(() => parseHeaders(['no colon here'])).toThrow()
})
test("throws on empty key", () => {
expect(() => parseHeaders([": value"])).toThrow();
});
test('throws on empty key', () => {
expect(() => parseHeaders([': value'])).toThrow()
})
test("handles value with colons (like URLs)", () => {
expect(parseHeaders(["url: http://example.com:8080"])).toEqual({
url: "http://example.com:8080",
});
});
test('handles value with colons (like URLs)', () => {
expect(parseHeaders(['url: http://example.com:8080'])).toEqual({
url: 'http://example.com:8080',
})
})
test("returns empty object for empty array", () => {
expect(parseHeaders([])).toEqual({});
});
test('returns empty object for empty array', () => {
expect(parseHeaders([])).toEqual({})
})
test("handles duplicate keys (last wins)", () => {
expect(parseHeaders(["K: v1", "K: v2"])).toEqual({ K: "v2" });
});
});
test('handles duplicate keys (last wins)', () => {
expect(parseHeaders(['K: v1', 'K: v2'])).toEqual({ K: 'v2' })
})
})

View File

@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { describe, expect, test } from 'bun:test'
import {
mcpInfoFromString,
buildMcpToolName,
@@ -6,135 +6,133 @@ import {
getMcpDisplayName,
getToolNameForPermissionCheck,
extractMcpToolDisplayName,
} from "../mcpStringUtils";
} from '../mcpStringUtils'
// ─── mcpInfoFromString ─────────────────────────────────────────────────
describe("mcpInfoFromString", () => {
test("parses standard mcp tool name", () => {
const result = mcpInfoFromString("mcp__github__list_issues");
expect(result).toEqual({ serverName: "github", toolName: "list_issues" });
});
describe('mcpInfoFromString', () => {
test('parses standard mcp tool name', () => {
const result = mcpInfoFromString('mcp__github__list_issues')
expect(result).toEqual({ serverName: 'github', toolName: 'list_issues' })
})
test("returns null for non-mcp string", () => {
expect(mcpInfoFromString("Bash")).toBeNull();
expect(mcpInfoFromString("grep__pattern")).toBeNull();
});
test('returns null for non-mcp string', () => {
expect(mcpInfoFromString('Bash')).toBeNull()
expect(mcpInfoFromString('grep__pattern')).toBeNull()
})
test("returns null when no server name", () => {
expect(mcpInfoFromString("mcp__")).toBeNull();
});
test('returns null when no server name', () => {
expect(mcpInfoFromString('mcp__')).toBeNull()
})
test("handles server name only (no tool)", () => {
const result = mcpInfoFromString("mcp__server");
expect(result).toEqual({ serverName: "server", toolName: undefined });
});
test('handles server name only (no tool)', () => {
const result = mcpInfoFromString('mcp__server')
expect(result).toEqual({ serverName: 'server', toolName: undefined })
})
test("preserves double underscores in tool name", () => {
const result = mcpInfoFromString("mcp__server__tool__with__underscores");
test('preserves double underscores in tool name', () => {
const result = mcpInfoFromString('mcp__server__tool__with__underscores')
expect(result).toEqual({
serverName: "server",
toolName: "tool__with__underscores",
});
});
serverName: 'server',
toolName: 'tool__with__underscores',
})
})
test("returns null for empty string", () => {
expect(mcpInfoFromString("")).toBeNull();
});
});
test('returns null for empty string', () => {
expect(mcpInfoFromString('')).toBeNull()
})
})
// ─── getMcpPrefix ──────────────────────────────────────────────────────
describe("getMcpPrefix", () => {
test("creates prefix from server name", () => {
expect(getMcpPrefix("github")).toBe("mcp__github__");
});
describe('getMcpPrefix', () => {
test('creates prefix from server name', () => {
expect(getMcpPrefix('github')).toBe('mcp__github__')
})
test("normalizes server name with special chars", () => {
expect(getMcpPrefix("my-server")).toBe("mcp__my-server__");
});
test('normalizes server name with special chars', () => {
expect(getMcpPrefix('my-server')).toBe('mcp__my-server__')
})
test("normalizes dots to underscores", () => {
expect(getMcpPrefix("my.server")).toBe("mcp__my_server__");
});
});
test('normalizes dots to underscores', () => {
expect(getMcpPrefix('my.server')).toBe('mcp__my_server__')
})
})
// ─── buildMcpToolName ──────────────────────────────────────────────────
describe("buildMcpToolName", () => {
test("builds fully qualified name", () => {
expect(buildMcpToolName("github", "list_issues")).toBe(
"mcp__github__list_issues"
);
});
describe('buildMcpToolName', () => {
test('builds fully qualified name', () => {
expect(buildMcpToolName('github', 'list_issues')).toBe(
'mcp__github__list_issues',
)
})
test("normalizes both server and tool names", () => {
expect(buildMcpToolName("my.server", "my.tool")).toBe(
"mcp__my_server__my_tool"
);
});
});
test('normalizes both server and tool names', () => {
expect(buildMcpToolName('my.server', 'my.tool')).toBe(
'mcp__my_server__my_tool',
)
})
})
// ─── getMcpDisplayName ─────────────────────────────────────────────────
describe("getMcpDisplayName", () => {
test("strips mcp prefix from full name", () => {
expect(getMcpDisplayName("mcp__github__list_issues", "github")).toBe(
"list_issues"
);
});
describe('getMcpDisplayName', () => {
test('strips mcp prefix from full name', () => {
expect(getMcpDisplayName('mcp__github__list_issues', 'github')).toBe(
'list_issues',
)
})
test("returns full name if prefix doesn't match", () => {
expect(getMcpDisplayName("mcp__other__tool", "github")).toBe(
"mcp__other__tool"
);
});
});
expect(getMcpDisplayName('mcp__other__tool', 'github')).toBe(
'mcp__other__tool',
)
})
})
// ─── getToolNameForPermissionCheck ─────────────────────────────────────
describe("getToolNameForPermissionCheck", () => {
test("returns built MCP name for MCP tools", () => {
describe('getToolNameForPermissionCheck', () => {
test('returns built MCP name for MCP tools', () => {
const tool = {
name: "list_issues",
mcpInfo: { serverName: "github", toolName: "list_issues" },
};
expect(getToolNameForPermissionCheck(tool)).toBe(
"mcp__github__list_issues"
);
});
name: 'list_issues',
mcpInfo: { serverName: 'github', toolName: 'list_issues' },
}
expect(getToolNameForPermissionCheck(tool)).toBe('mcp__github__list_issues')
})
test("returns tool name for non-MCP tools", () => {
const tool = { name: "Bash" };
expect(getToolNameForPermissionCheck(tool)).toBe("Bash");
});
test('returns tool name for non-MCP tools', () => {
const tool = { name: 'Bash' }
expect(getToolNameForPermissionCheck(tool)).toBe('Bash')
})
test("returns tool name when mcpInfo is undefined", () => {
const tool = { name: "Write", mcpInfo: undefined };
expect(getToolNameForPermissionCheck(tool)).toBe("Write");
});
});
test('returns tool name when mcpInfo is undefined', () => {
const tool = { name: 'Write', mcpInfo: undefined }
expect(getToolNameForPermissionCheck(tool)).toBe('Write')
})
})
// ─── extractMcpToolDisplayName ─────────────────────────────────────────
describe("extractMcpToolDisplayName", () => {
test("extracts display name from full user-facing name", () => {
describe('extractMcpToolDisplayName', () => {
test('extracts display name from full user-facing name', () => {
expect(
extractMcpToolDisplayName("github - Add comment to issue (MCP)")
).toBe("Add comment to issue");
});
extractMcpToolDisplayName('github - Add comment to issue (MCP)'),
).toBe('Add comment to issue')
})
test("removes (MCP) suffix only", () => {
expect(extractMcpToolDisplayName("simple-tool (MCP)")).toBe("simple-tool");
});
test('removes (MCP) suffix only', () => {
expect(extractMcpToolDisplayName('simple-tool (MCP)')).toBe('simple-tool')
})
test("handles name without (MCP) suffix", () => {
expect(extractMcpToolDisplayName("github - List issues")).toBe(
"List issues"
);
});
test('handles name without (MCP) suffix', () => {
expect(extractMcpToolDisplayName('github - List issues')).toBe(
'List issues',
)
})
test("handles name without dash separator", () => {
expect(extractMcpToolDisplayName("just-a-name")).toBe("just-a-name");
});
});
test('handles name without dash separator', () => {
expect(extractMcpToolDisplayName('just-a-name')).toBe('just-a-name')
})
})

View File

@@ -1,59 +1,59 @@
import { describe, expect, test } from "bun:test";
import { normalizeNameForMCP } from "../normalization";
import { describe, expect, test } from 'bun:test'
import { normalizeNameForMCP } from '../normalization'
describe("normalizeNameForMCP", () => {
test("returns simple valid name unchanged", () => {
expect(normalizeNameForMCP("my-server")).toBe("my-server");
});
describe('normalizeNameForMCP', () => {
test('returns simple valid name unchanged', () => {
expect(normalizeNameForMCP('my-server')).toBe('my-server')
})
test("replaces dots with underscores", () => {
expect(normalizeNameForMCP("my.server.name")).toBe("my_server_name");
});
test('replaces dots with underscores', () => {
expect(normalizeNameForMCP('my.server.name')).toBe('my_server_name')
})
test("replaces spaces with underscores", () => {
expect(normalizeNameForMCP("my server")).toBe("my_server");
});
test('replaces spaces with underscores', () => {
expect(normalizeNameForMCP('my server')).toBe('my_server')
})
test("replaces special characters with underscores", () => {
expect(normalizeNameForMCP("server@v2!")).toBe("server_v2_");
});
test('replaces special characters with underscores', () => {
expect(normalizeNameForMCP('server@v2!')).toBe('server_v2_')
})
test("returns already valid name unchanged", () => {
expect(normalizeNameForMCP("valid_name-123")).toBe("valid_name-123");
});
test('returns already valid name unchanged', () => {
expect(normalizeNameForMCP('valid_name-123')).toBe('valid_name-123')
})
test("returns empty string for empty input", () => {
expect(normalizeNameForMCP("")).toBe("");
});
test('returns empty string for empty input', () => {
expect(normalizeNameForMCP('')).toBe('')
})
test("handles claude.ai prefix: collapses consecutive underscores and strips edges", () => {
test('handles claude.ai prefix: collapses consecutive underscores and strips edges', () => {
// "claude.ai My Server" -> replace invalid -> "claude_ai_My_Server"
// starts with "claude.ai " so collapse + strip -> "claude_ai_My_Server"
expect(normalizeNameForMCP("claude.ai My Server")).toBe(
"claude_ai_My_Server"
);
});
expect(normalizeNameForMCP('claude.ai My Server')).toBe(
'claude_ai_My_Server',
)
})
test("handles claude.ai prefix with consecutive invalid chars", () => {
test('handles claude.ai prefix with consecutive invalid chars', () => {
// "claude.ai ...test..." -> replace invalid -> "claude_ai____test___"
// collapse consecutive _ -> "claude_ai_test_"
// strip leading/trailing _ -> "claude_ai_test"
expect(normalizeNameForMCP("claude.ai ...test...")).toBe("claude_ai_test");
});
expect(normalizeNameForMCP('claude.ai ...test...')).toBe('claude_ai_test')
})
test("non-claude.ai name preserves consecutive underscores", () => {
test('non-claude.ai name preserves consecutive underscores', () => {
// "a..b" -> "a__b", no claude.ai prefix so no collapse
expect(normalizeNameForMCP("a..b")).toBe("a__b");
});
expect(normalizeNameForMCP('a..b')).toBe('a__b')
})
test("non-claude.ai name preserves trailing underscores", () => {
expect(normalizeNameForMCP("name!")).toBe("name_");
});
test('non-claude.ai name preserves trailing underscores', () => {
expect(normalizeNameForMCP('name!')).toBe('name_')
})
test("handles claude.ai prefix that results in only underscores", () => {
test('handles claude.ai prefix that results in only underscores', () => {
// "claude.ai ..." -> replace invalid -> "claude_ai____"
// collapse -> "claude_ai_"
// strip trailing -> "claude_ai"
expect(normalizeNameForMCP("claude.ai ...")).toBe("claude_ai");
});
});
expect(normalizeNameForMCP('claude.ai ...')).toBe('claude_ai')
})
})

View File

@@ -1,41 +1,41 @@
import { mock, describe, expect, test, afterEach } from "bun:test";
import { debugMock } from "../../../../tests/mocks/debug";
import { mock, describe, expect, test, afterEach } from 'bun:test'
import { debugMock } from '../../../../tests/mocks/debug'
mock.module("axios", () => ({
mock.module('axios', () => ({
default: { get: async () => ({ data: { servers: [] } }) },
}));
mock.module("src/utils/debug.ts", debugMock);
}))
mock.module('src/utils/debug.ts', debugMock)
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
"../officialRegistry"
);
'../officialRegistry'
)
describe("isOfficialMcpUrl", () => {
describe('isOfficialMcpUrl', () => {
afterEach(() => {
resetOfficialMcpUrlsForTesting();
});
resetOfficialMcpUrlsForTesting()
})
test("returns false when registry not loaded (initial state)", () => {
resetOfficialMcpUrlsForTesting();
expect(isOfficialMcpUrl("https://example.com")).toBe(false);
});
test('returns false when registry not loaded (initial state)', () => {
resetOfficialMcpUrlsForTesting()
expect(isOfficialMcpUrl('https://example.com')).toBe(false)
})
test("returns false for non-registered URL", () => {
expect(isOfficialMcpUrl("https://random-server.com/mcp")).toBe(false);
});
test('returns false for non-registered URL', () => {
expect(isOfficialMcpUrl('https://random-server.com/mcp')).toBe(false)
})
test("returns false for empty string", () => {
expect(isOfficialMcpUrl("")).toBe(false);
});
});
test('returns false for empty string', () => {
expect(isOfficialMcpUrl('')).toBe(false)
})
})
describe("resetOfficialMcpUrlsForTesting", () => {
test("can be called without error", () => {
expect(() => resetOfficialMcpUrlsForTesting()).not.toThrow();
});
describe('resetOfficialMcpUrlsForTesting', () => {
test('can be called without error', () => {
expect(() => resetOfficialMcpUrlsForTesting()).not.toThrow()
})
test("clears state so subsequent lookups return false", () => {
resetOfficialMcpUrlsForTesting();
expect(isOfficialMcpUrl("https://anything.com")).toBe(false);
});
});
test('clears state so subsequent lookups return false', () => {
resetOfficialMcpUrlsForTesting()
expect(isOfficialMcpUrl('https://anything.com')).toBe(false)
})
})

View File

@@ -12,7 +12,13 @@ import {
export function createMcpAnalytics(): AnalyticsSink {
return {
trackEvent(event: string, metadata: Record<string, unknown>) {
logEvent(event, metadata as Record<string, AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS>)
logEvent(
event,
metadata as Record<
string,
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
>,
)
},
}
}

View File

@@ -9,7 +9,11 @@ import { maybeResizeAndDownsampleImageBuffer } from '../../../utils/imageResizer
export function createMcpImageProcessor(): ImageProcessor {
return {
async resizeAndDownsample(buffer: Buffer) {
const result = await maybeResizeAndDownsampleImageBuffer(buffer, buffer.length, 'png')
const result = await maybeResizeAndDownsampleImageBuffer(
buffer,
buffer.length,
'png',
)
return result.buffer
},
}

View File

@@ -2,7 +2,10 @@
import type { ContentStorage } from '@claude-code-best/mcp-client'
import { persistBinaryContent } from '../../../utils/mcpOutputStorage.js'
import { persistToolResult, isPersistError } from '../../../utils/toolResultStorage.js'
import {
persistToolResult,
isPersistError,
} from '../../../utils/toolResultStorage.js'
/**
* Creates a ContentStorage implementation using the host's binary persistence.
@@ -10,7 +13,11 @@ import { persistToolResult, isPersistError } from '../../../utils/toolResultStor
export function createMcpStorage(): ContentStorage {
return {
async persistBinaryContent(data: Buffer, ext: string) {
const result = await persistBinaryContent(data, ext, `mcp-adapter-${Date.now()}`)
const result = await persistBinaryContent(
data,
ext,
`mcp-adapter-${Date.now()}`,
)
if ('error' in result) {
throw new Error(result.error)
}

View File

@@ -69,7 +69,7 @@ export function isChannelAllowlisted(
if (!pluginSource) return false
const { name, marketplace } = parsePluginIdentifier(pluginSource)
if (!marketplace) return false
if (marketplace === BUILTIN_MARKETPLACE_NAME && name === 'weixin') {
if (marketplace === BUILTIN_MARKETPLACE_NAME && name === 'weixin') {
return true
}
return getChannelAllowlist().some(

View File

@@ -20,9 +20,7 @@ import type { ServerCapabilities } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod/v4'
import { type ChannelEntry, getAllowedChannels } from '../../bootstrap/state.js'
import { CHANNEL_TAG } from '../../constants/xml.js'
import {
getSubscriptionType,
} from '../../utils/auth.js'
import { getSubscriptionType } from '../../utils/auth.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js'
import { getSettingsForSource } from '../../utils/settings/settings.js'
@@ -259,7 +257,6 @@ export function gateChannelServer(
reason: `you asked for plugin:${entry.name}@${entry.marketplace} but the installed ${entry.name} plugin is from ${actual ?? 'an unknown source'}`,
}
}
}
return { action: 'register' }

View File

@@ -51,7 +51,10 @@ import {
toolMatchesName,
} from '../../Tool.js'
import { ListMcpResourcesTool } from '@claude-code-best/builtin-tools/tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
import { type MCPProgress, MCPTool } from '@claude-code-best/builtin-tools/tools/MCPTool/MCPTool.js'
import {
type MCPProgress,
MCPTool,
} from '@claude-code-best/builtin-tools/tools/MCPTool/MCPTool.js'
import { createMcpAuthTool } from '@claude-code-best/builtin-tools/tools/McpAuthTool/McpAuthTool.js'
import { ReadMcpResourceTool } from '@claude-code-best/builtin-tools/tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
import { createAbortController } from '../../utils/abortController.js'
@@ -903,7 +906,8 @@ export const connectToServer = memoize(
)
logMCPDebug(name, `claude.ai proxy transport created successfully`)
} else if (
((serverRef as ScopedMcpServerConfig).type === 'stdio' || !(serverRef as ScopedMcpServerConfig).type) &&
((serverRef as ScopedMcpServerConfig).type === 'stdio' ||
!(serverRef as ScopedMcpServerConfig).type) &&
isClaudeInChromeMCPServer(name)
) {
// Run the Chrome MCP server in-process to avoid spawning a ~325 MB subprocess
@@ -916,7 +920,9 @@ export const connectToServer = memoize(
const { createLinkedTransportPair } = await import(
'./InProcessTransport.js'
)
const context = createChromeContext((serverRef as McpStdioServerConfig).env)
const context = createChromeContext(
(serverRef as McpStdioServerConfig).env,
)
inProcessServer = createClaudeForChromeMcpServer(context)
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
@@ -924,7 +930,8 @@ export const connectToServer = memoize(
logMCPDebug(name, `In-process Chrome MCP server started`)
} else if (
feature('CHICAGO_MCP') &&
((serverRef as ScopedMcpServerConfig).type === 'stdio' || !(serverRef as ScopedMcpServerConfig).type) &&
((serverRef as ScopedMcpServerConfig).type === 'stdio' ||
!(serverRef as ScopedMcpServerConfig).type) &&
isComputerUseMCPServer!(name)
) {
// Run the Computer Use MCP server in-process — same rationale as
@@ -941,7 +948,10 @@ export const connectToServer = memoize(
await inProcessServer.connect(serverTransport)
transport = clientTransport
logMCPDebug(name, `In-process Computer Use MCP server started`)
} else if ((serverRef as ScopedMcpServerConfig).type === 'stdio' || !(serverRef as ScopedMcpServerConfig).type) {
} else if (
(serverRef as ScopedMcpServerConfig).type === 'stdio' ||
!(serverRef as ScopedMcpServerConfig).type
) {
const stdioRef = serverRef as McpStdioServerConfig
const finalCommand =
process.env.CLAUDE_CODE_SHELL_PREFIX || stdioRef.command
@@ -958,7 +968,9 @@ export const connectToServer = memoize(
stderr: 'pipe', // prevents error output from the MCP server from printing to the UI
})
} else {
throw new Error(`Unsupported server type: ${(serverRef as ScopedMcpServerConfig).type}`)
throw new Error(
`Unsupported server type: ${(serverRef as ScopedMcpServerConfig).type}`,
)
}
// Set up stderr logging for stdio transport before connecting in case there are any stderr
@@ -3247,8 +3259,14 @@ async function callMCPTool({
}
function extractToolUseId(message: AssistantMessage): string | undefined {
const firstBlock = (message.message.content as ContentBlockParam[] | undefined)?.[0]
if (!firstBlock || typeof firstBlock === 'string' || firstBlock.type !== 'tool_use') {
const firstBlock = (
message.message.content as ContentBlockParam[] | undefined
)?.[0]
if (
!firstBlock ||
typeof firstBlock === 'string' ||
firstBlock.type !== 'tool_use'
) {
return undefined
}
return firstBlock.id

View File

@@ -1351,7 +1351,7 @@ export function parseMcpConfig(params: {
if (
getPlatform() === 'windows' &&
(!configToCheck.type || configToCheck.type === 'stdio') &&
('command' in configToCheck) &&
'command' in configToCheck &&
(configToCheck.command === 'npx' ||
configToCheck.command.endsWith('\\npx') ||
configToCheck.command.endsWith('/npx'))

View File

@@ -56,8 +56,7 @@ export async function findAvailablePort(): Promise<number> {
})
})
return port
} catch {
}
} catch {}
}
// If random selection failed, try the fallback port

View File

@@ -536,9 +536,7 @@ export function useManageMCPConnections(
// reply and emits {request_id, behavior}; no regex on our
// side, text in the general channel can't accidentally match.
if (
client.capabilities?.experimental?.[
'claude/channel/permission'
]
client.capabilities?.experimental?.['claude/channel/permission']
) {
client.client.setNotificationHandler(
ChannelPermissionNotificationSchema(),
@@ -567,9 +565,7 @@ export function useManageMCPConnections(
client.client.removeNotificationHandler(
'notifications/claude/channel',
)
client.client.removeNotificationHandler(
CHANNEL_PERMISSION_METHOD,
)
client.client.removeNotificationHandler(CHANNEL_PERMISSION_METHOD)
logMCPDebug(
client.name,
`Channel notifications skipped: ${gate.reason}`,