diff --git a/package.json b/package.json index cc4ab731..258d4fb2 100644 --- a/package.json +++ b/package.json @@ -190,6 +190,12 @@ "description": "The XML Language server allows other VSCode extensions to extend its functionality. It requires Java-specific features in order to do this. If extensions to the XML language server are detected, but a binary XML language server is run, a warning will appear. This setting can be used to disable this warning.", "scope": "window" }, + "xml.mcp.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable multi-client mode. When enabled, the XML language server listens on a TCP socket and registers itself in `${workspace}/.lsp-servers/lemminx.json`, allowing MCP servers (like languagetools) to connect and share the same server instance. Requires restart to take effect.", + "scope": "window" + }, "xml.server.binary.path": { "type": "string", "description": "Specify the path of a custom binary version of the XML server to use. A binary will be downloaded if this is not set.", diff --git a/src/client/xmlClient.ts b/src/client/xmlClient.ts index 15da34ea..d9805726 100644 --- a/src/client/xmlClient.ts +++ b/src/client/xmlClient.ts @@ -1,7 +1,7 @@ import { TelemetryEvent } from '@redhat-developer/vscode-redhat-telemetry/lib'; import { commands, ExtensionContext, extensions, Position, TextDocument, TextEditor, Uri, window, workspace } from 'vscode'; import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, DocumentFilter, DocumentSelector, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, State, TextDocumentPositionParams } from "vscode-languageclient"; -import { Executable, LanguageClient } from 'vscode-languageclient/node'; +import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; import { XMLFileAssociation } from '../api/xmlExtensionApi'; import { registerClientServerCommands } from '../commands/registerCommands'; import * as ServerCommandConstants from '../commands/serverCommandConstants'; @@ -41,10 +41,10 @@ const ActionableNotification = new NotificationType('xml/acti let languageClient: LanguageClient; -export async function startLanguageClient(context: ExtensionContext, executable: Executable, logfile: string, externalXmlSettings: ExternalXmlSettings, requirementsData: RequirementsData): Promise { +export async function startLanguageClient(context: ExtensionContext, serverOptions: ServerOptions, logfile: string, externalXmlSettings: ExternalXmlSettings, requirementsData: RequirementsData): Promise { const languageClientOptions: LanguageClientOptions = getLanguageClientOptions(logfile, externalXmlSettings, requirementsData, context); - languageClient = new LanguageClient('xml', 'XML Support', executable, languageClientOptions); + languageClient = new LanguageClient('xml', 'XML Support', serverOptions, languageClientOptions); //In vscode-languageclient version 9.0.0, inline completion (textDocument/inlineCompletion) is a proposed feature // TODO remove registerProposedFeatures once upgraded to 10.x languageClient.registerProposedFeatures(); diff --git a/src/extension.ts b/src/extension.ts index e9c1d189..dd22a6e0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,7 +12,7 @@ import * as fs from 'fs-extra'; import { ConfigurationTarget, ExtensionContext, Uri, commands, extensions, languages, window, workspace } from "vscode"; -import { Executable, LanguageClient } from 'vscode-languageclient/node'; +import { Executable, LanguageClient, ServerOptions } from 'vscode-languageclient/node'; import { XMLExtensionApi } from './api/xmlExtensionApi'; import { getXmlExtensionApiImplementation } from './api/xmlExtensionApiImplementation'; import { cleanUpHeapDumps } from './client/clientErrorHandler'; @@ -67,7 +67,7 @@ export async function activate(context: ExtensionContext): Promise { + const mcpEnabled = getXMLConfiguration().get('mcp.enabled', false); + const workspacePath = workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + + // Always use stdio mode, but pass MCP parameters if enabled return { command: path.resolve(requirements.java_home + '/bin/java'), - args: prepareParams(requirements, xmlJavaExtensions, context) + args: prepareParams(requirements, xmlJavaExtensions, context, mcpEnabled, workspacePath) } as Executable; } -function prepareParams(requirements: RequirementsData, xmlJavaExtensions: string[], context: ExtensionContext): string[] { +function prepareParams(requirements: RequirementsData, xmlJavaExtensions: string[], context: ExtensionContext, mcpEnabled?: boolean, workspacePath?: string): string[] { const params: string[] = []; if (DEBUG) { if (process.env['SUSPEND_SERVER'] === 'true') { @@ -96,7 +100,17 @@ function prepareParams(requirements: RequirementsData, xmlJavaExtensions: string xmlJavaExtensionsClasspath = pathSeparator + xmlJavaExtensions.join(pathSeparator); } params.push('-cp'); params.push(path.resolve(server_home, launchersFound[0]) + xmlJavaExtensionsClasspath); + + // Always use stdio launcher params.push('org.eclipse.lemminx.XMLServerLauncher'); + + // Add MCP arguments if enabled + if (mcpEnabled && workspacePath) { + params.push('--mcp-enabled'); + params.push('--workspace'); params.push(workspacePath); + params.push('--client-name'); params.push(env.appName); // e.g., "Visual Studio Code", "VSCodium" + params.push('--client-version'); params.push(vscodeVersion); + } } else { return null; } @@ -135,3 +149,5 @@ export function parseVMargs(params: any[], vmargsLine: string) { } }); } + + diff --git a/src/server/serverStarter.ts b/src/server/serverStarter.ts index 0cfbb9f8..af2a33a6 100644 --- a/src/server/serverStarter.ts +++ b/src/server/serverStarter.ts @@ -1,5 +1,5 @@ import { commands, ConfigurationTarget, ExtensionContext, window } from "vscode"; -import { Executable } from "vscode-languageclient/node"; +import { Executable, ServerOptions } from "vscode-languageclient/node"; import { getXMLConfiguration } from "../settings/settings"; import * as Telemetry from "../telemetry"; import { ABORTED_ERROR, prepareBinaryExecutable } from "./binary/binaryServerStarter"; @@ -7,18 +7,18 @@ import { prepareJavaExecutable } from "./java/javaServerStarter"; import { getOpenJDKDownloadLink, RequirementsData } from "./requirements"; /** - * Returns the executable to use to launch LemMinX (the XML Language Server) + * Returns the server options to use to launch LemMinX (the XML Language Server) * * @param requirements the java information, or an empty object if there is no java * @param xmlJavaExtensions a list of all the java extension jars * @param context the extensions context * @throws if neither the binary nor the java version of the extension can be launched - * @returns the executable to launch LemMinX with (the XML language server) + * @returns the server options to launch LemMinX with (the XML language server) */ export async function prepareExecutable( requirements: RequirementsData, xmlJavaExtensions: string[], - context: ExtensionContext): Promise { + context: ExtensionContext): Promise { const hasJava: boolean = requirements.java_home !== undefined; const hasExtensions: boolean = xmlJavaExtensions.length !== 0;