Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
52 changes: 47 additions & 5 deletions packages/cli/src/commands/mcp/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ describe('mcp list command', () => {
'test-server': { command: '/test/server' },
},
},
isTrusted: true,
});

mockClient.connect.mockRejectedValue(new Error('Connection failed'));
Expand Down Expand Up @@ -371,7 +372,7 @@ describe('mcp list command', () => {
);
});

it('should show stdio servers as disconnected in untrusted folders', async () => {
it('should show stdio servers as disabled in untrusted folders', async () => {
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
mockedLoadSettings.mockReturnValue({
merged: {
Expand All @@ -383,16 +384,16 @@ describe('mcp list command', () => {
isTrusted: false,
});

// createTransport will throw in core if not trusted
mockedCreateTransport.mockRejectedValue(new Error('Folder not trusted'));

await listMcpServers();

expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining(
'test-server: /test/server (stdio) - Disconnected',
'Warning: MCP servers are configured but disabled because this folder is untrusted.',
),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining('test-server: /test/server (stdio) - Disabled'),
);
});

it('should display blocked status for servers in excluded list', async () => {
Expand Down Expand Up @@ -446,4 +447,45 @@ describe('mcp list command', () => {
);
expect(mockedCreateTransport).not.toHaveBeenCalled();
});

it('should display warning and disabled status in untrusted folders', async () => {
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const mcpServers = {
'project-server': { command: '/path/to/project/server' },
'user-server': { url: 'https://example.com/user' },
};

mockedLoadSettings.mockReturnValue({
merged: {
...defaultMergedSettings,
mcpServers: {
'user-server': mcpServers['user-server'],
},
},
isTrusted: false,
getMergedSettingsAsIfTrusted: () => ({
...defaultMergedSettings,
mcpServers,
}),
});

await listMcpServers();

expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining(
'Warning: MCP servers are configured but disabled because this folder is untrusted.',
),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining(
'project-server: /path/to/project/server (stdio) - Disabled',
),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect.stringContaining(
'user-server: https://example.com/user (http) - Disabled',
),
);
expect(mockedCreateTransport).not.toHaveBeenCalled();
});
});
22 changes: 21 additions & 1 deletion packages/cli/src/commands/mcp/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ async function getServerStatus(
return MCPServerStatus.DISABLED;
}

if (!isTrusted) {
return MCPServerStatus.DISABLED;
}

// Test all server types by attempting actual connection
return testMCPConnection(serverName, server, isTrusted, activeSettings);
}
Expand All @@ -189,8 +193,15 @@ export async function listMcpServers(
const loadedSettings = loadedSettingsArg ?? loadSettings();
const activeSettings = loadedSettings.merged;

// If the folder is untrusted, we want to show all configured servers (including
// project-scoped ones) as disabled.
const allSettings =
!loadedSettings.isTrusted && loadedSettings.getMergedSettingsAsIfTrusted
? loadedSettings.getMergedSettingsAsIfTrusted()
: activeSettings;

const { mcpServers, blockedServerNames } =
await getMcpServersFromConfig(activeSettings);
await getMcpServersFromConfig(allSettings);
const serverNames = Object.keys(mcpServers);

if (blockedServerNames.length > 0) {
Expand All @@ -208,6 +219,15 @@ export async function listMcpServers(
return;
}

if (!loadedSettings.isTrusted) {
debugLogger.log(
chalk.yellow(
'Warning: MCP servers are configured but disabled because this folder is untrusted.\n' +
'User-level servers are also suppressed in untrusted folders to prevent accidental side-effects.\n',
),
);
}

debugLogger.log('Configured MCP servers:\n');

for (const serverName of serverNames) {
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,15 @@ export class LoadedSettings {
return this._merged;
}

/**
* Returns a merged settings object as if the folder were trusted.
* This is useful for commands like 'mcp list' that want to show
* what's configured even if it's currently disabled for security reasons.
*/
getMergedSettingsAsIfTrusted(): MergedSettings {
return this.computeMergedSettings(true);
}

setTrusted(isTrusted: boolean): void {
if (this.isTrusted === isTrusted) {
return;
Expand All @@ -368,13 +377,16 @@ export class LoadedSettings {
};
}

private computeMergedSettings(): MergedSettings {
private computeMergedSettings(forceTrusted = false): MergedSettings {
const isTrusted = forceTrusted || this.isTrusted;
const workspace = forceTrusted ? this._workspaceFile : this.workspace;

const merged = mergeSettings(
this.system.settings,
this.systemDefaults.settings,
this.user.settings,
this.workspace.settings,
this.isTrusted,
workspace.settings,
isTrusted,
);

// Remote admin settings always take precedence and file-based admin settings
Expand Down
Loading