Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to auto-start MCP servers on frontend start-up #14736

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { FrontendApplicationContribution, PreferenceProvider, PreferenceService } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MCPServerDescription, MCPServerManager } from '../common';
Expand All @@ -24,6 +25,7 @@ interface MCPServersPreferenceValue {
command: string;
args?: string[];
env?: { [key: string]: string };
autostart?: boolean;
};

interface MCPServersPreference {
Expand All @@ -35,7 +37,8 @@ namespace MCPServersPreference {
return !!obj && typeof obj === 'object' &&
'command' in obj && typeof obj.command === 'string' &&
(!('args' in obj) || Array.isArray(obj.args) && obj.args.every(arg => typeof arg === 'string')) &&
(!('env' in obj) || !!obj.env && typeof obj.env === 'object' && Object.values(obj.env).every(value => typeof value === 'string'));
(!('env' in obj) || !!obj.env && typeof obj.env === 'object' && Object.values(obj.env).every(value => typeof value === 'string')) &&
(!('autostart' in obj) || typeof obj.autostart === 'boolean');
}
}

Expand Down Expand Up @@ -72,8 +75,9 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo
MCP_SERVERS_PREF,
{}
));
this.syncServers(servers);
this.prevServers = this.convertToMap(servers);
this.syncServers(this.prevServers);
this.autoStartServers(this.prevServers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some usability questions:

  • This will only auto start servers when the frontend is started. Should we also auto start servers which are added to the preferences with autostart: true or when their autostart value is added or changed?
  • If a frontend is started with a server set to autostart: false (or non existing), should we then stop the server in case it is already running?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.: I decided to not start them on preference change (this is documented in the settings). Reasoning was that if you start a server manually, you get the message with the functions. If you set up a new server, you probably want to start it manually once. If you have a strong feeling to change this behavior, its fine, but I found it a bit more logical this way.
2.: I decided no, because this would break other frontends that still use this server. Also, "removing auto start", does not imply "stop it" to me. You can still manually stop them, though.


this.preferenceService.onPreferenceChanged(event => {
if (event.preferenceName === MCP_SERVERS_PREF) {
Expand All @@ -84,6 +88,17 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo
this.frontendMCPService.registerToolsForAllStartedServers();
}

protected async autoStartServers(servers: Map<string, MCPServerDescription>): Promise<void> {
const startedServers = await this.frontendMCPService.getStartedServers();
for (const [name, serverDesc] of servers) {
if (serverDesc && serverDesc.autostart) {
if (!startedServers.includes(name)) {
await this.frontendMCPService.startServer(name);
}
}
}
}

protected handleServerChanges(newServers: MCPServersPreference): void {
const oldServers = this.prevServers;
const updatedServers = this.convertToMap(newServers);
Expand Down Expand Up @@ -116,20 +131,17 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo
this.prevServers = updatedServers;
}

protected syncServers(servers: MCPServersPreference): void {
const updatedServers = this.convertToMap(servers);
protected syncServers(servers: Map<string, MCPServerDescription>): void {

for (const [, description] of updatedServers) {
for (const [, description] of servers) {
this.manager.addOrUpdateServer(description);
}

for (const [name] of this.prevServers) {
if (!updatedServers.has(name)) {
if (!servers.has(name)) {
this.manager.removeServer(name);
}
}

this.prevServers = updatedServers;
}

protected convertToMap(servers: MCPServersPreference): Map<string, MCPServerDescription> {
Expand Down
19 changes: 13 additions & 6 deletions packages/ai-mcp/src/browser/mcp-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const McpServersPreferenceSchema: PreferenceSchema = {
[MCP_SERVERS_PREF]: {
type: 'object',
title: 'MCP Servers Configuration',
markdownDescription: 'Configure MCP servers with command, arguments and optionally environment variables. Each server is identified by a unique key, such as\
"brave-search" or "filesystem".\
markdownDescription: 'Configure MCP servers with command, arguments, optionally environment variables, and autostart.\
Each server is identified by a unique key, such as "brave-search" or "filesystem".\
To start a server, use the "MCP: Start MCP Server" command, which enables you to select the desired server.\
To stop a server, use the "MCP: Stop MCP Server" command.\
\n\
Expand All @@ -40,17 +40,18 @@ export const McpServersPreferenceSchema: PreferenceSchema = {
],\n\
"env": {\n\
"BRAVE_API_KEY": "YOUR_API_KEY"\n\
}\n\
},\n\
"autostart": true\n\
},\n\
"filesystem": {\n\
"command": "npx",\n\
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/YOUR_USERNAME/Desktop"],\n\
"env": {\n\
"CUSTOM_ENV_VAR": "custom-value"\n\
}\n\
},\n\
"autostart": false\n\
}\n\
}\
```',
}\n ```',
additionalProperties: {
type: 'object',
properties: {
Expand All @@ -74,6 +75,12 @@ export const McpServersPreferenceSchema: PreferenceSchema = {
additionalProperties: {
type: 'string'
}
},
autostart: {
type: 'boolean',
title: 'Autostart',
markdownDescription: 'Automatically start this server when the frontend starts. Newly added servers are not immediatly auto stated.',
default: false
}
},
required: ['command', 'args']
Expand Down
5 changes: 5 additions & 0 deletions packages/ai-mcp/src/common/mcp-server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export interface MCPServerDescription {
* Optional environment variables to set when starting the server.
*/
env?: { [key: string]: string };

/**
* Flag indicating whether the server should automatically start when the application starts.
*/
autostart?: boolean;
}

export const MCPServerManager = Symbol('MCPServerManager');
Expand Down
Loading