Skip to content

Commit 444aaf6

Browse files
authored
mcp: feedback on autostart (microsoft#258650)
- Fixes microsoft#258445 (error when sending a request) - Show verbose progress if MCP startup takes >5s - Don't show the server button for new tools if autostart is enabled
1 parent 481029b commit 444aaf6

File tree

7 files changed

+87
-35
lines changed

7 files changed

+87
-35
lines changed

src/vs/workbench/contrib/chat/common/chatServiceImpl.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ import { localize } from '../../../../nls.js';
2222
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
2323
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
2424
import { ILogService } from '../../../../platform/log/common/log.js';
25-
import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
2625
import { Progress } from '../../../../platform/progress/common/progress.js';
2726
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
2827
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
2928
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
3029
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
31-
import { IMcpServer, IMcpService, McpConnectionState, McpServerCacheState, McpStartServerInteraction } from '../../mcp/common/mcpTypes.js';
32-
import { startServerAndWaitForLiveTools } from '../../mcp/common/mcpTypesUtils.js';
30+
import { IMcpService } from '../../mcp/common/mcpTypes.js';
3331
import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js';
3432
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js';
3533
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js';
@@ -774,31 +772,6 @@ export class ChatService extends Disposable implements IChatService {
774772
return newTokenSource.token;
775773
}
776774

777-
private async _checkForMcpAutostart(token: CancellationToken) {
778-
let todo: IMcpServer[] = [];
779-
780-
const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);
781-
782-
// don't try re-running errored servers, let the user choose if they want that
783-
const candidates = this.mcpService.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);
784-
785-
if (autoStartConfig === McpAutoStartValue.OnlyNew) {
786-
todo = candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown);
787-
} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {
788-
todo = candidates.filter(s => {
789-
const c = s.cacheState.get();
790-
return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;
791-
});
792-
}
793-
794-
if (!todo.length) {
795-
return;
796-
}
797-
798-
const interaction = new McpStartServerInteraction();
799-
await Promise.all(todo.map(server => startServerAndWaitForLiveTools(server, { interaction }, token)));
800-
}
801-
802775
private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {
803776
const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId);
804777
let request: ChatRequestModel;
@@ -851,6 +824,10 @@ export class ChatService extends Disposable implements IChatService {
851824
const stopWatch = new StopWatch(false);
852825
store.add(token.onCancellationRequested(() => {
853826
this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);
827+
if (!request) {
828+
return;
829+
}
830+
854831
this.telemetryService.publicLog2<ChatProviderInvokedEvent, ChatProviderInvokedClassification>('interactiveSessionProviderInvoked', {
855832
timeToFirstProgress: undefined,
856833
// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling
@@ -952,7 +929,7 @@ export class ChatService extends Disposable implements IChatService {
952929
const command = detectedCommand ?? agentSlashCommandPart?.command;
953930
await Promise.all([
954931
this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`),
955-
this._checkForMcpAutostart(token),
932+
this.mcpService.autostart(token),
956933
]);
957934

958935
// Recompute history in case the agent or command changed

src/vs/workbench/contrib/mcp/browser/mcpCommands.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ export class ListMcpServerCommand extends Action2 {
8383
f1: true,
8484
menu: [{
8585
when: ContextKeyExpr.and(
86-
ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors),
86+
ContextKeyExpr.or(
87+
ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools),
88+
McpContextKeys.hasServersWithErrors,
89+
),
8790
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
8891
ChatContextKeys.lockedToCodingAgent.negate()
8992
),
@@ -511,7 +514,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
511514
});
512515

513516
const single = servers.length === 1;
514-
const names = servers.map(s => isServer(s) ? link(s) : '`' + s.label + '`').map(l => single ? l : `- ${l}\n`).join(', ');
517+
const names = servers.map(s => isServer(s) ? link(s) : '`' + s.label + '`').map(l => single ? l : `- ${l}`).join('\n');
515518
let markdown: MarkdownString;
516519
if (state === DisplayedState.NewTools) {
517520
markdown = new MarkdownString(single

src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { localize } from '../../../../nls.js';
1010
import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
1111
import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js';
1212
import { IWorkbenchContribution } from '../../../common/contributions.js';
13-
import { LazyCollectionState, IMcpService, McpServerCacheState, McpConnectionState } from './mcpTypes.js';
13+
import { IMcpService, LazyCollectionState, McpConnectionState, McpServerCacheState } from './mcpTypes.js';
1414

1515

1616
export namespace McpContextKeys {
@@ -35,7 +35,7 @@ export class McpContextKeysController extends Disposable implements IWorkbenchCo
3535

3636
constructor(
3737
@IMcpService mcpService: IMcpService,
38-
@IContextKeyService contextKeyService: IContextKeyService
38+
@IContextKeyService contextKeyService: IContextKeyService,
3939
) {
4040
super();
4141

src/vs/workbench/contrib/mcp/common/mcpService.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { RunOnceScheduler } from '../../../../base/common/async.js';
7+
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
78
import { Disposable } from '../../../../base/common/lifecycle.js';
89
import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';
10+
import { localize } from '../../../../nls.js';
11+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
12+
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
913
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1014
import { ILogService } from '../../../../platform/log/common/log.js';
15+
import { mcpAutoStartConfig, McpAutoStartValue } from '../../../../platform/mcp/common/mcpManagement.js';
16+
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
1117
import { StorageScope } from '../../../../platform/storage/common/storage.js';
1218
import { IMcpRegistry } from './mcpRegistryTypes.js';
1319
import { McpServer, McpServerMetadataCache } from './mcpServer.js';
14-
import { IMcpServer, IMcpService, McpCollectionDefinition, McpServerCacheState, McpServerDefinition, McpToolName } from './mcpTypes.js';
20+
import { IMcpServer, IMcpService, McpCollectionDefinition, McpConnectionState, McpServerCacheState, McpServerDefinition, McpStartServerInteraction, McpToolName } from './mcpTypes.js';
21+
import { startServerAndWaitForLiveTools } from './mcpTypesUtils.js';
1522

1623
type IMcpServerRec = { object: IMcpServer; toolPrefix: string };
1724

@@ -31,6 +38,9 @@ export class McpService extends Disposable implements IMcpService {
3138
@IInstantiationService private readonly _instantiationService: IInstantiationService,
3239
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
3340
@ILogService private readonly _logService: ILogService,
41+
@IProgressService private readonly progressService: IProgressService,
42+
@ICommandService private readonly commandService: ICommandService,
43+
@IConfigurationService private readonly configurationService: IConfigurationService
3444
) {
3545
super();
3646

@@ -49,6 +59,58 @@ export class McpService extends Disposable implements IMcpService {
4959
}));
5060
}
5161

62+
public async autostart(token?: CancellationToken): Promise<void> {
63+
const autoStartConfig = this.configurationService.getValue<McpAutoStartValue>(mcpAutoStartConfig);
64+
65+
// don't try re-running errored servers, let the user choose if they want that
66+
const candidates = this.servers.get().filter(s => s.connectionState.get().state !== McpConnectionState.Kind.Error);
67+
68+
let todo: IMcpServer[] = [];
69+
if (autoStartConfig === McpAutoStartValue.OnlyNew) {
70+
todo = candidates.filter(s => s.cacheState.get() === McpServerCacheState.Unknown);
71+
} else if (autoStartConfig === McpAutoStartValue.NewAndOutdated) {
72+
todo = candidates.filter(s => {
73+
const c = s.cacheState.get();
74+
return c === McpServerCacheState.Unknown || c === McpServerCacheState.Outdated;
75+
});
76+
}
77+
78+
if (!todo.length) {
79+
return;
80+
}
81+
82+
const interaction = new McpStartServerInteraction();
83+
const cts = new CancellationTokenSource(token);
84+
85+
await this.progressService.withProgress(
86+
{
87+
location: ProgressLocation.Notification,
88+
cancellable: true,
89+
delay: 5_000,
90+
total: todo.length,
91+
buttons: [localize('mcp.autostart.configure', 'Configure MCP Autostart')]
92+
},
93+
report => {
94+
const remaining = new Set(todo);
95+
const doReport = () => report.report({ message: localize('mcp.autostart.progress', 'Starting MCP server: {0}', [...remaining].map(r => r.definition.label).join(', ')), total: todo.length, increment: 1 });
96+
doReport();
97+
return Promise.all(todo.map(async server => {
98+
await startServerAndWaitForLiveTools(server, { interaction }, cts.token);
99+
remaining.delete(server);
100+
doReport();
101+
}));
102+
},
103+
btn => {
104+
if (btn === 0) {
105+
this.commandService.executeCommand('workbench.action.openSettings', mcpAutoStartConfig);
106+
}
107+
cts.cancel();
108+
},
109+
);
110+
111+
cts.dispose();
112+
}
113+
52114
public resetCaches(): void {
53115
this.userCache.reset();
54116
this.workspaceCache.reset();

src/vs/workbench/contrib/mcp/common/mcpTypes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ export interface IMcpService {
209209

210210
/** Set if there are extensions that register MCP servers that have never been activated. */
211211
readonly lazyCollectionState: IObservable<{ state: LazyCollectionState; collections: McpCollectionDefinition[] }>;
212+
213+
/** Auto-starts pending servers based on user settings. */
214+
autostart(token?: CancellationToken): Promise<void>;
215+
212216
/** Activatese extensions and runs their MCP servers. */
213217
activateCollections(): Promise<void>;
214218
}

src/vs/workbench/contrib/mcp/test/common/mcpResourceFilesystem.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as assert from 'assert';
77
import { Barrier, timeout } from '../../../../../base/common/async.js';
88
import { URI } from '../../../../../base/common/uri.js';
99
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10+
import { NullCommandService } from '../../../../../platform/commands/test/common/nullCommandService.js';
11+
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
1012
import { FileChangeType, FileSystemProviderErrorCode, FileType, IFileChange, IFileService } from '../../../../../platform/files/common/files.js';
1113
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
1214
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
@@ -46,7 +48,7 @@ suite('Workbench - MCP - ResourceFilesystem', () => {
4648
const registry = new TestMcpRegistry(parentInsta1);
4749

4850
const parentInsta2 = ds.add(parentInsta1.createChild(new ServiceCollection([IMcpRegistry, registry])));
49-
const mcpService = ds.add(new McpService(parentInsta2, registry, new NullLogService()));
51+
const mcpService = ds.add(new McpService(parentInsta2, registry, new NullLogService(), {} as any, NullCommandService, new TestConfigurationService()));
5052
mcpService.updateCollectedServers();
5153

5254
const instaService = ds.add(parentInsta2.createChild(new ServiceCollection(

src/vs/workbench/contrib/mcp/test/common/testMcpService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export class TestMcpService implements IMcpService {
1616

1717
}
1818

19+
autostart(): Promise<void> {
20+
return Promise.resolve();
21+
}
22+
1923
public lazyCollectionState = observableValue(this, { state: LazyCollectionState.AllKnown, collections: [] });
2024

2125
activateCollections(): Promise<void> {

0 commit comments

Comments
 (0)