From 156a1bef9d1deff25afd3df360163d6f3b154226 Mon Sep 17 00:00:00 2001 From: azerr Date: Fri, 12 Jun 2026 19:20:52 +0200 Subject: [PATCH] Qute MCP server Signed-off-by: azerr --- Jenkinsfile | 15 + buildAll.bat | 1 + buildAll.sh | 1 + .../.mvn/wrapper/MavenWrapperDownloader.java | 98 ++++ .../.mvn/wrapper/maven-wrapper.properties | 18 + lsp4j-mcp/README.md | 514 ++++++++++++++++++ .../docs/screenshots/bob-diagnostics.png | Bin 0 -> 87665 bytes lsp4j-mcp/docs/screenshots/bob-prompt.png | Bin 0 -> 71328 bytes lsp4j-mcp/mvnw | 308 +++++++++++ lsp4j-mcp/mvnw.cmd | 205 +++++++ lsp4j-mcp/pom.xml | 108 ++++ .../java/com/redhat/lsp4j/mcp/GSonUtils.java | 49 ++ .../redhat/lsp4j/mcp/annotations/Inject.java | 26 + .../lsp4j/mcp/annotations/RequireDidOpen.java | 45 ++ .../redhat/lsp4j/mcp/annotations/Tool.java | 36 ++ .../redhat/lsp4j/mcp/annotations/ToolArg.java | 35 ++ .../com/redhat/lsp4j/mcp/cache/McpCache.java | 111 ++++ .../redhat/lsp4j/mcp/server/LspMcpServer.java | 352 ++++++++++++ .../mcp/server/McpLanguageClientWrapper.java | 72 +++ .../mcp/server/McpLanguageServerWrapper.java | 77 +++ .../server/McpTextDocumentServiceWrapper.java | 360 ++++++++++++ .../lsp4j/mcp/server/McpToolHandler.java | 31 ++ .../lsp4j/mcp/server/McpToolRegistry.java | 445 +++++++++++++++ .../com/redhat/lsp4j/mcp/tools/McpTool.java | 23 + .../com/redhat/lsp4j/mcp/tools/Position.java | 50 ++ .../mcp/tools/TextDocumentIdentifier.java | 39 ++ .../mcp/tools/lsp/LspGetCodeActionsTool.java | 142 +++++ .../mcp/tools/lsp/LspGetDiagnosticsTool.java | 41 ++ .../com.redhat.lsp4j.mcp.tools.McpTool | 3 + .../server/McpJsonSchemaGenerationTest.java | 357 ++++++++++++ .../McpMethodArgsDeserializationTest.java | 274 ++++++++++ qute.ls/com.redhat.qute.ls/pom.xml | 54 +- .../redhat/qute/ls/QuteServerLauncher.java | 221 ++++---- .../qute/ls/api/QuteLanguageServerAPI.java | 8 +- .../roq/data/yaml/YamlDataLoader.java | 10 +- 35 files changed, 4024 insertions(+), 105 deletions(-) create mode 100644 lsp4j-mcp/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 lsp4j-mcp/.mvn/wrapper/maven-wrapper.properties create mode 100644 lsp4j-mcp/README.md create mode 100644 lsp4j-mcp/docs/screenshots/bob-diagnostics.png create mode 100644 lsp4j-mcp/docs/screenshots/bob-prompt.png create mode 100644 lsp4j-mcp/mvnw create mode 100644 lsp4j-mcp/mvnw.cmd create mode 100644 lsp4j-mcp/pom.xml create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/GSonUtils.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Inject.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/RequireDidOpen.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Tool.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/ToolArg.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/cache/McpCache.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/LspMcpServer.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageClientWrapper.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageServerWrapper.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpTextDocumentServiceWrapper.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolHandler.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolRegistry.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/McpTool.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/Position.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/TextDocumentIdentifier.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetCodeActionsTool.java create mode 100644 lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetDiagnosticsTool.java create mode 100644 lsp4j-mcp/src/main/resources/META-INF/services/com.redhat.lsp4j.mcp.tools.McpTool create mode 100644 lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpJsonSchemaGenerationTest.java create mode 100644 lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpMethodArgsDeserializationTest.java diff --git a/Jenkinsfile b/Jenkinsfile index 442f936a2..f301c013c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -15,6 +15,7 @@ pipeline { sh ''' export JAVA_HOME="${NATIVE_TOOLS}${SEP}openjdk17_last" MVN="${COMMON_TOOLS}${SEP}maven3-latest/bin/mvn -V -Dmaven.repo.local=${WORKSPACE}/.repository/ -B -ntp" + ${MVN} -f ${WORKSPACE}/lsp4j-mcp/pom.xml versions:set -DnewVersion=$VERSION -DnewVersion=${VERSION} ${MVN} -f ${WORKSPACE}/quarkus.jdt.ext/pom.xml org.eclipse.tycho:tycho-versions-plugin:1.7.0:set-version -DnewVersion=${VERSION} -Dtycho.mode=maven ${MVN} -f ${WORKSPACE}/quarkus.ls.ext/com.redhat.quarkus.ls/pom.xml versions:set -DnewVersion=$VERSION -DnewVersion=${VERSION} -Dtycho.mode=maven ${MVN} -f ${WORKSPACE}/qute.jdt/pom.xml org.eclipse.tycho:tycho-versions-plugin:1.7.0:set-version -DnewVersion=${VERSION} -Dtycho.mode=maven @@ -31,6 +32,20 @@ pipeline { export JAVA_HOME="${NATIVE_TOOLS}${SEP}jdk11_last" MVN="${COMMON_TOOLS}${SEP}maven3-latest/bin/mvn -V -Dmaven.repo.local=${WORKSPACE}/.repository/" + pom=${WORKSPACE}/lsp4j-mcp/pom.xml + + pomVersion=$(grep "" ${pom} | head -1 | sed -e "s#.*\\(.\\+\\).*#\\1#") + if [[ ${pomVersion} == *"-SNAPSHOT" ]]; then + ${MVN} clean deploy ${mvnFlags} -f ${pom} + else + ${MVN} clean deploy ${mvnFlags} -DskipRemoteStaging=true -f ${pom} \ + -DstagingDescription="[${JOB_NAME} ${BUILD_TIMESTAMP} ${BUILD_NUMBER}] :: ${pomVersion} :: deploy to local" + ${MVN} nexus-staging:deploy-staged ${mvnFlags} -f ${pom} \ + -DstagingDescription="[${JOB_NAME} ${BUILD_TIMESTAMP} ${BUILD_NUMBER}] :: ${pomVersion} :: deploy to stage + close" + ${MVN} nexus-staging:release ${mvnFlags} -f ${pom} \ + -DstagingDescription="[${JOB_NAME} ${BUILD_TIMESTAMP} ${BUILD_NUMBER}] :: ${pomVersion} :: release" + fi + pom=${WORKSPACE}/quarkus.ls.ext/com.redhat.quarkus.ls/pom.xml pomVersion=$(grep "" ${pom} | head -1 | sed -e "s#.*\\(.\\+\\).*#\\1#") diff --git a/buildAll.bat b/buildAll.bat index a2cf1f784..323630e49 100755 --- a/buildAll.bat +++ b/buildAll.bat @@ -1,3 +1,4 @@ +cd lsp4j-mcp && .\mvnw.cmd clean install && cd .. ^ cd quarkus.jdt.ext && .\mvnw.cmd clean verify && cd .. ^ cd quarkus.ls.ext\com.redhat.quarkus.ls && .\mvnw.cmd clean verify && cd .. ^ cd qute.jdt && .\mvnw.cmd clean verify && cd .. ^ diff --git a/buildAll.sh b/buildAll.sh index 9bd815630..e3133c6fe 100755 --- a/buildAll.sh +++ b/buildAll.sh @@ -1,3 +1,4 @@ +cd lsp4j-mcp && ./mvnw clean verify && cd .. cd quarkus.jdt.ext && ./mvnw clean verify && cd .. cd quarkus.ls.ext/com.redhat.quarkus.ls && ./mvnw clean verify && cd ../.. cd qute.jdt && ./mvnw clean verify && cd .. diff --git a/lsp4j-mcp/.mvn/wrapper/MavenWrapperDownloader.java b/lsp4j-mcp/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 000000000..84d1e60d8 --- /dev/null +++ b/lsp4j-mcp/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public final class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.2.0"; + + private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) ); + + public static void main( String[] args ) + { + log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION ); + + if ( args.length != 2 ) + { + System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" ); + System.exit( 1 ); + } + + try + { + log( " - Downloader started" ); + final URL wrapperUrl = new URL( args[0] ); + final String jarPath = args[1].replace( "..", "" ); // Sanitize path + final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize(); + downloadFileFromURL( wrapperUrl, wrapperJarPath ); + log( "Done" ); + } + catch ( IOException e ) + { + System.err.println( "- Error downloading: " + e.getMessage() ); + if ( VERBOSE ) + { + e.printStackTrace(); + } + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath ) + throws IOException + { + log( " - Downloading to: " + wrapperJarPath ); + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + final String username = System.getenv( "MVNW_USERNAME" ); + final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + try ( InputStream inStream = wrapperUrl.openStream() ) + { + Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING ); + } + log( " - Downloader complete" ); + } + + private static void log( String msg ) + { + if ( VERBOSE ) + { + System.out.println( msg ); + } + } + +} diff --git a/lsp4j-mcp/.mvn/wrapper/maven-wrapper.properties b/lsp4j-mcp/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..ac184013f --- /dev/null +++ b/lsp4j-mcp/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/lsp4j-mcp/README.md b/lsp4j-mcp/README.md new file mode 100644 index 000000000..5fcc2e638 --- /dev/null +++ b/lsp4j-mcp/README.md @@ -0,0 +1,514 @@ +# lsp4j-mcp + +**The Problem: Using an IDE and an MCP Client Together** + +When you want to use an **IDE** ([VS Code](https://code.visualstudio.com/), [Bob IDE](https://bob.ibm.com/docs/ide), [JetBrains](https://www.jetbrains.com/), etc.) alongside an **MCP client** ([Bob Shell](https://bob.ibm.com/docs/shell), [Claude Code](https://claude.com/fr/product/claude-code), etc.), traditional MCP server approaches create serious problems: they launch a separate Language Server instance instead of using your IDE's, leading to double parsing, file synchronization issues, settings mismatch, and invisible unsaved changes. + +**The lsp4j-mcp Solution** + +lsp4j-mcp solves this by embedding a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server **inside** your LSP4J-based language server. This allows your IDE and MCP clients to connect to the **same Language Server instance**, sharing diagnostics, code actions, and all LSP features without launching a separate process. + +## The Problem: MCP + LSP + IDE Don't Play Well Together + +Typically, MCP servers that work with Language Servers act as the **LSP client** and launch their own Language Server process. This works fine for standalone MCP clients (like Claude Desktop without an IDE), but creates serious problems when you want to use an **IDE** alongside an **MCP client** (Claude, [Bob Shell](https://bob.ibm.com/docs/shell), etc.): + +### Problems with the Traditional Approach + +When the MCP server launches its own Language Server instance: + +- ❌ **Separate Language Server process** - Not using the IDE's Language Server instance +- ❌ **Double parsing** - Two Language Server processes analyzing the same code +- ❌ **Duplicate configuration** - The MCP client must configure the LSP client separately +- ❌ **Custom commands management** - MCP server must handle language-specific commands +- ❌ **Settings mismatch** - Language Server settings differ from the IDE's settings +- ❌ **Unsaved changes invisible** - Content being edited but not saved is not accessible to the MCP server +- ❌ **Resource waste** - Two processes doing the same work + +### The lsp4j-mcp Solution + +**Reverse the relationship:** Instead of the MCP server launching the Language Server, the **Language Server embeds the MCP server**. + +``` +Traditional (problematic): lsp4j-mcp (solution): +MCP Server → Language Server IDE → Language Server (with embedded MCP Server) + ↑ ↑ + | | + IDE MCP Client (Claude) +``` + +**Benefits:** +- ✅ Single Language Server instance shared by IDE and MCP client (same as the IDE's) +- ✅ Same diagnostics, settings, and state everywhere +- ✅ Unsaved changes visible to AI assistant +- ✅ No duplicate parsing or configuration +- ✅ Works with any IDE ([Bob](https://bob.ibm.com/docs/ide), VS Code, JetBrains, etc.) + +## What is MCP? + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is a protocol that allows AI assistants to interact with external tools and data sources. By exposing LSP diagnostics and code actions through MCP, AI assistants can: + +- Query compilation errors, warnings, and linting issues in real-time +- Suggest fixes based on available code actions +- Understand project context better when answering questions + +## Use Case: Qute Template Validation with Bob + +Imagine you're working on a Qute template project in [Bob](https://bob.ibm.com/docs/ide) (an AI-powered IDE). You ask: + +> **User:** "Are there any errors in my Qute project?" + +![Bob prompt screenshot](docs/screenshots/bob-prompt.png) +*Screenshot: User asking Bob about errors in Qute project* + +Bob connects to the Qute Language Server via MCP and retrieves diagnostics: + +![Bob diagnostics result](docs/screenshots/bob-diagnostics.png) +*Screenshot: Bob displaying Qute template errors returned by the MCP server* + +This is possible because the Qute Language Server embeds an MCP server that exposes its diagnostics as tools. + +## Alternative MCP Integration Approaches + +For reference, here are other approaches to integrate MCP with language servers and their limitations: + +### Approach 1: MCP Server Launches Language Server (Traditional) + +**Example:** [stephanj/LSP4J-MCP](https://github.com/stephanj/LSP4J-MCP) + +The MCP server acts as the LSP client and launches its own Language Server process. + +**Why this doesn't work well with IDEs:** +- ❌ Creates a **separate Language Server instance** (not the IDE's) +- ❌ **Double parsing** and resource waste +- ❌ **File synchronization issues** - IDE and MCP server have different views +- ❌ **Unsaved changes invisible** - Only saved files are accessible +- ❌ **Settings mismatch** - Different configuration from the IDE +- ❌ **Custom commands** must be handled by the MCP server + +### Approach 2: IDE-based MCP Server + +**Example:** JetBrains IDE MCP Server ([docs](https://www.jetbrains.com/help/idea/mcp-server.html)) + +The IDE itself provides an MCP server that exposes IDE features (including LSP data) as tools. + +**Limitations:** +- ❌ Requires a specific IDE to be running (JetBrains only) +- ❌ Not portable to other editors (VS Code, [Bob](https://bob.ibm.com/docs/ide), etc.) +- ❌ Tied to IDE-specific extension points + +### Approach 3: Language Server Launches MCP Server ✅ (lsp4j-mcp) + +**Our solution:** + +``` +Step 1: IDE launches Language Server +──────────────────────────────────── + +┌────────────────┐ +│ IDE / Editor │ +│ (Bob, VS Code, │ +│ JetBrains) │ +└───────┬────────┘ + │ + │ Launches LS process (one per workspace) + ▼ +┌─────────────────────────────────────────────────────┐ +│ Language Server Process │ +│ │ +│ ┌────────────────────┐ │ +│ │ LSP Server │ │ +│ │ (Qute LS, JDT LS) │ │ +│ └────────────────────┘ │ +└─────────────────────────────────────────────────────┘ + + +Step 2: Language Server embeds MCP Server +────────────────────────────────────────── + +┌────────────────┐ ┌────────────────┐ +│ IDE / Editor │ │ AI Assistant │ +│ (Bob, VS Code, │ │ (Claude) │ +│ JetBrains) │ └───────┬────────┘ +└───────┬────────┘ │ + │ │ + │ LSP Protocol │ MCP Protocol + ▼ ▼ +┌─────────────────────────────────────────────────────┐ +│ Language Server Process (per workspace) │ +│ │ +│ ┌────────────────────┐ ┌──────────────────┐ │ +│ │ LSP Server │◄────►│ MCP Server │ │ +│ │ (Qute LS, JDT LS) │ │ (lsp4j-mcp) │ │ +│ └────────────────────┘ └──────────────────┘ │ +│ │ │ │ +│ └───────────┬───────────────┘ │ +│ ▼ │ +│ Shared diagnostics, │ +│ code actions, cache │ +└─────────────────────────────────────────────────────┘ +``` + +**How it works:** + +1. **IDE launches the language server** (standard LSP workflow) + - One language server process per workspace/project + - IDE is the LSP client, controls the language server lifecycle + +2. **Language server embeds an MCP server** (using lsp4j-mcp) + - MCP server runs in the same process as the language server + - Exposes language server features as MCP tools + +3. **Multiple clients, one language server** + - IDE connects via LSP (for editor features) + - AI assistant connects via MCP (for diagnostics, code actions) + - Both share the same language server instance + +**Scaling:** +- N Bob windows → N language server processes → N MCP servers +- Each workspace gets its own language server + MCP server pair + +**Benefits:** +- ✅ Works with **any IDE** that supports LSP ([Bob](https://bob.ibm.com/docs/ide), VS Code, JetBrains, Emacs, etc.) +- ✅ **IDE controls the language server lifecycle** (launch, shutdown) +- ✅ Shares the same language server instance the IDE is using +- ✅ No file synchronization issues - single source of truth +- ✅ One MCP server per workspace/project +- ✅ Diagnostics are always in sync with the IDE +- ✅ Language server doesn't need to know about the IDE implementation + +**Key insight:** + +The **IDE launches the language server** (not the MCP server). The MCP server is just an additional interface embedded inside the language server process, allowing AI assistants to access the same data the IDE sees. + +## Getting Started + +### 1. Add Dependency + +Add lsp4j-mcp to your LSP4J-based language server: + +```xml + + com.redhat.lsp4j + lsp4j-mcp + 0.1.0-SNAPSHOT + +``` + +### 2. Create MCP Tools + +Define tools using annotations: + +```java +package com.example.tools; + +import com.redhat.lsp4j.mcp.annotations.*; +import com.redhat.lsp4j.mcp.tools.McpTool; +import org.eclipse.lsp4j.services.LanguageServer; + +public class MyCustomTool implements McpTool { + + @Inject + private LanguageServer languageServer; + + @Tool(description = "Get diagnostics for a file") + public List getDiagnostics( + @ToolArg(name = "uri", description = "File URI") String uri) { + + // Your tool implementation + return diagnostics; + } +} +``` + +### 3. Register via ServiceLoader + +Create `src/main/resources/META-INF/services/com.redhat.lsp4j.mcp.tools.McpTool`: + +``` +com.example.tools.MyCustomTool +com.redhat.lsp4j.mcp.tools.lsp.LspGetDiagnosticsTool +com.redhat.lsp4j.mcp.tools.lsp.LspGetCodeActionsTool +``` + +### 4. Launch MCP Server in Your Language Server + +```java +import com.redhat.lsp4j.mcp.server.LspMcpServer; + +public class MyLanguageServer implements LanguageServer { + + private LspMcpServer mcpServer; + + @Override + public CompletableFuture initialize(InitializeParams params) { + // ... your initialization code + + // Start MCP server + mcpServer = new LspMcpServer(this, 9339); + mcpServer.start(); + + return CompletableFuture.completedFuture(result); + } + + @Override + public CompletableFuture shutdown() { + if (mcpServer != null) { + mcpServer.stop(); + } + return CompletableFuture.completedFuture(null); + } +} +``` + +### 5. Connect from AI Assistant + +Configure your AI assistant (e.g., Claude Desktop) to connect to the MCP server: + +```json +{ + "mcpServers": { + "qute-ls": { + "type": "sse", + "url": "http://localhost:9339/mcp/sse" + } + } +} +``` + +## Built-in LSP Tools + +lsp4j-mcp provides generic LSP tools out of the box: + +### `get_diagnostics` + +Get compilation errors, warnings, and other diagnostics for a file. + +**Schema:** +```json +{ + "textDocument": { + "uri": "file:///path/to/file.html" + } +} +``` + +**Features:** +- Automatically calls `textDocument/didOpen` if the file isn't open +- Caches diagnostics published by the language server +- Cleans up with `textDocument/didClose` after execution + +### `get_code_actions` + +Get available code actions (quick fixes) at a position. + +**Schema:** +```json +{ + "textDocument": { + "uri": "file:///path/to/file.html" + }, + "position": { + "line": 10, + "character": 5 + } +} +``` + +## Annotation Reference + +### `@Tool` + +Marks a method as an MCP tool. + +```java +@Tool( + name = "custom_name", // Optional: tool name (default: camelCase → snake_case) + description = "Tool description" +) +public ReturnType methodName(params...) { } +``` + +### `@ToolArg` + +Describes a method parameter or nested object field. + +```java +public void myTool( + @ToolArg( + name = "paramName", // Optional: parameter name (default: arg0, arg1, ...) + description = "Parameter description" + ) String param) { } +``` + +For nested objects, annotate the **getters**: + +```java +public class Position { + private int line; + + @ToolArg(description = "Line number (0-based)") + public int getLine() { return line; } +} +``` + +### `@Inject` + +Injects dependencies into tool instances. + +```java +public class MyTool implements McpTool { + @Inject + private LanguageServer languageServer; + + @Inject + private McpCache cache; +} +``` + +Available dependencies: +- `LanguageServer` - The LSP4J language server instance +- `McpCache` - Diagnostic and file state cache + +### `@RequireDidOpen` + +Many LSP features require that a file be opened first: `textDocument/publishDiagnostics`, `textDocument/codeAction`, `textDocument/completion`, etc. The Language Server only processes and publishes diagnostics for files that have been opened via `textDocument/didOpen`. + +This annotation automatically calls `textDocument/didOpen` before tool execution **only if the file is not already open in the IDE**, and calls `textDocument/didClose` afterward to clean up. This ensures: +- MCP clients can query diagnostics for files that aren't currently open in the IDE +- Files already open in the IDE are not unnecessarily reopened (preserving IDE state) + +```java +@Tool(description = "Get diagnostics") +@RequireDidOpen( + uriParam = "textDocument.uri", // Path to URI in arguments + languageId = "qute-html" // Language ID for didOpen +) +public List getDiagnostics(TextDocumentIdentifier textDocument) { } +``` + +**How it works:** +1. Checks if the file is already open in the IDE (tracked by the Language Server) +2. If **not open**: calls `textDocument/didOpen` → executes tool → calls `textDocument/didClose` +3. If **already open**: executes tool directly (IDE manages the file lifecycle) + +## How It Works + +### JSON Schema Generation + +lsp4j-mcp automatically generates JSON schemas from your Java method signatures: + +```java +@Tool(description = "Example") +public void example( + @ToolArg(name = "text", description = "Input") String text, + @ToolArg(name = "position", description = "Position") Position position) { } +``` + +Generates: + +```json +{ + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "Input" + }, + "position": { + "type": "object", + "properties": { + "line": { "type": "integer", "description": "Line number" }, + "character": { "type": "integer", "description": "Character offset" } + }, + "required": ["line", "character"] + } + }, + "required": ["text", "position"] +} +``` + +### Argument Deserialization + +MCP arguments (JSON) are automatically deserialized into Java objects using Gson: + +```json +{ + "textDocument": { + "uri": "file:///path/to/file.html" + }, + "position": { + "line": 10, + "character": 5 + } +} +``` + +→ Deserialized to `TextDocumentIdentifier` and `Position` objects. + +**Important:** For Gson to deserialize correctly: +- Classes must have **private fields** (not just getters) +- Don't extend LSP4J classes directly (create standalone DTOs) + +Example: + +```java +// ✅ Good - Gson can deserialize +public class TextDocumentIdentifier { + private String uri; // Direct field access + + @ToolArg(description = "File URI") + public String getUri() { return uri; } + + public void setUri(String uri) { this.uri = uri; } +} + +// ❌ Bad - Gson can't find fields +public class TextDocumentIdentifier extends org.eclipse.lsp4j.TextDocumentIdentifier { + // Fields are in parent class, Gson won't find them +} +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Language Server Process │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ LSP Server │ │ MCP Server │ │ +│ │ │ │ (Undertow SSE) │ │ +│ │ ┌──────────────┐ │ │ │ │ +│ │ │ Text Doc Svc │◄┼─────────┤ Tool Handler │ │ +│ │ └──────────────┘ │ │ │ │ +│ │ │ │ ┌──────────────┐ │ │ +│ │ ┌──────────────┐ │ │ │ Tool Registry│ │ │ +│ │ │ Diagnostics │◄┼─────────┤ │ │ │ │ +│ │ └──────────────┘ │ │ │ @Tool scan │ │ │ +│ └──────────────────┘ │ │ @Inject DI │ │ │ +│ │ │ │ ServiceLoader│ │ │ +│ │ │ └──────────────┘ │ │ +│ ┌────────▼─────────┐ │ │ │ +│ │ McpCache │◄────────┤ Cache │ │ +│ │ - diagnostics │ │ │ │ +│ │ - didOpen state │ └──────────────────┘ │ +│ └──────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + ▲ │ + │ LSP │ MCP (SSE) + │ ▼ + ┌────┴────┐ ┌──────────┐ + │ IDE │ │ Claude │ + └─────────┘ └──────────┘ +``` + +## Examples + +See the [Qute Language Server](https://github.com/redhat-developer/quarkus-ls/tree/master/qute.ls) for a complete integration example. + +## License + +EPL-2.0 + +## Contributing + +Contributions welcome! Please open an issue or PR. diff --git a/lsp4j-mcp/docs/screenshots/bob-diagnostics.png b/lsp4j-mcp/docs/screenshots/bob-diagnostics.png new file mode 100644 index 0000000000000000000000000000000000000000..4e0b9eabcd478aece9ad1ab891b5c78907cc85bf GIT binary patch literal 87665 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

yPm^yyvt7%pXqwAIOFX-473C+*JYBW6#UWM5@w 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 IbEkFx0~WP}$p8QV literal 0 HcmV?d00001 diff --git a/lsp4j-mcp/docs/screenshots/bob-prompt.png b/lsp4j-mcp/docs/screenshots/bob-prompt.png new file mode 100644 index 0000000000000000000000000000000000000000..ca9d94df8d727b35bb9534af3833774d9800841a GIT binary patch literal 71328 zcmce-WmuG7yFNS&HGseXB1j|B5+c$uL2vB8?h3IJO13Iw=egKy3K9do_>EbU19qk{G2wJMpBh7iz$n*D<~qOi%THv z0bb~01o^zLU_1l9G*K8N;g|| z(bRmcIkt1u_&UExwEDw=5v@N|93o4YUO^7I|0^Ky5cc;9#s6Qw^u(}8;nM_NkDY3V zlRuh*`*%O^MJZ(9_u{-#l4C%=>rJAcai@AFxb0SBb=A*GDR?ZT>@t;II_T`TA zE$zw^7Zh&SUERn0F22_nWvnZ3Z&9hH;Wg%qK4kS_V^Z~@H)bFV@=Fub>&YDDd9c--amDtB`K7}xlpfPHl%Cw-)_$3_ zU7{TQ8OB#Q7~k1CN7I>(R(o-lHErgXU1#0aRA?Nc-YvEVqpa!w0z*!!*0M%MMogTm z48craaVLooVrG3Q%+=M^hijsD{LWt9=Rc@T4wrxV98Z{Yp!o0gqhL~~oPgJ3HTCSr zjQWMoVmhh4h065Z7Cw?J&->xKJkzaJNlH!@tOB)}pB;2kbG&VEQ9MySuFQ@QklU?W zOAB%4J{Q@p=wY`w!$W*Ko%8li-IBF5|DW z?~FEvmW24P&7+MY9`WCjMhDQPf2!;k;E2+%^ZDG_i$zCqZgeGd*hS}aEcX(UVZZbf zy$>sEA)I#>La{`|fNcA4d$G3SPo%^oWA_lX3C-La!F+qs@7=vVn7tChS`ww2E&U6L zf|{aL*=dqJ0$%^4sUNGREc{wqTN`{OfPId)kfiI_`_jh~+X(IMFuu8KBR`#Ld@TCI z&B@rz8UBvXRnL5@-%1+}N6fI2VDAm2)#aKkSArFT7-rlV;d~ZeR8lV|9Y(s)&a&wR z4Bg637z-BURL7={-dSIAZfCSO`2ji4Gc)Re!3QoIiew{~6RLnmFzCZ*XT(Vm#kK6~OL6e0w)P1c{McXhc(f8G*}HiW;Waa{e4 zwXCBwzl=k`^@j$DO!gdLwT3;#+=$CUQ2jyx z`B059+FscYyw8nivpQ*Bmm4kif8;#)?&PwCCo{gF4}?8_vApN}t!M1LX!PD6uP0N~ zw1-=CB5|qsZW8qmwAg0w_1Qe|KWO58dCP`Leffz(=j_fzvaxI2SgKD*!{m4Q8Ag-A zN&uKiCKo5esGs8jcE+O5y3e@}1Unf6n9XZBu6wgKU!EyEbb2@t&)NNPPpXM`TaH!^ zgc5dpeY59X`h~C0sw(iPlzTE8_lq{j@XNwCxDf5!&Nua%E&J$et8Z#6@o%NO_Ucn< z8=M3sZmX^m594j}yKNfMnT6}qX&df5bG93=n1@{ZadJ&twj8=!NS3QU_iPEV)j={i z*GTD+yY*Y_8)K#E4F;e)KI8mRzhAGxT{7?Ydgj|`&H&J6RBd? z3XYHd7gDw%E!y@%gULQ>;fl5R{&El=<&-_@V9m7aoabAwb=I2V<4Z{_x<;OxtFw5~ zn+@f{Q>nMx9JV{5P|9=tB}Em@X^?|Oc5TZsNgt&ewhMJ0g@E(sa-Nz~@+hS}p4X(p z9GwCqmD_Uk&^KFs5_nGMybyt{-=LVcKi1UqYM*1MVU3=>ua{jr0lbzpAJH>p8A0&K zqj$$MZ^VQUW|r<*LsUohBUl7a>|xv^Bdj^B#-^sv9Wv~F#A76Dp_VbW$sJM*Yms~8 z(#LlFgL{8+WIr!gkLk(hig{$>?$$2h@%A=ul{Dn#@m1qO@dGy8m;D??fa}v2i&XQr zaQ(CNn4ew>J4;lV&OIX$5`lQQRdmu6_IL$!eJE#@pOpldt+9F3$YVl`&{WvR=oLT< z?ho0J+CP=!8Y7ytn_S!PK2zh3Gl}k;1zg*-TvcpkDCbN?Kk=YG38v6ue5J7nR$((TDn9)$;9f0n}2Ef2=yX9ln;HtdF<_)0F3~v%^`?a+^XSZ zR0s7UE<_hSYLrBo_O1IyZlBu%gFl|%P9ho<%(t*SZ{<8oVSxx#XQ2_9f4HC95-&Vl z?Xeilm;!CZB?qIQj2PQz4{^i@HzOiCs#+ETsdsC#*#;s&JDB;Az~Z$aCOfL^irzn9jq;>!Crt$fe?btInXN{}g z*GtrNrEcQ|+J!-cuyxnkCen>zYLt%-9WMzFEIuq%Eym2O4qB++{x;`jF^0ZoR4Olv2F1Rc67gGZ z@R)ffI_(`{)qF>Bv2yEZl8?IDm45jJCCyF+MN{Zf z(wrf=lISX=EFo1*cGCsfRH1knYrc5#vLKWc4-W+&lh`B6D>plsW|0)t+jA0XC_fKrSpLP?daqA&rpi`0(t(U4uptNc^rPl1VYp z5;xx(zSk|4a0J&`a-XM~7cCFFkqM-x)Yh~vji!TRln_jUu9Wy!Vn#%I$)^U>K4eG^ z(`hw1uJtjOr^|v?h9dZGvqIVB$alo*FE8ECJX>A+`d@q|4Fh0I5fHp6U_12hq{&@Hoj@c5_mnXW^lez zVvjw_4`xN$`L%FVmu-|(_6e|ZYOBksgFXtQ=kY$_vs8f33+)`p_;E)o6L zox=5pI7%^A0&d9Q*S>dJ&BD#Pk*_;zo*)Mp ztwl)r#gfNAggZc2ubsK^+d90w?PgRoh0UxvbX}7lSwmg)J}5qnIM{pRHM?Ivo+b+- z9**FNWBrON7miI6=D!g8W>g>L{CU-9Y#QBo;nQ)t!+LZ5_NGq|nuo7+Jp3Ps*aYc% z53?QKVv?Kw$ctR=E-H!k!{22*qVn$cJgqy((^*EMUUs443onY4YMuD#e)wg|W%a@D z0p+NdPo}-1&qnD!)Aj)X@hE{aPL;U=gV)47fCKO_B~VFX;ir$afSf_;$H>u{H2sp! z_X6JC^kIJyd^dIeh&m>eqpl>?_$sScFupo{5$#0!=H>Vszie7LD=S}O*aaez=EF!` zmICC7E@lv8Sb_oCgKVh;hB7+kT<{nguWk@$1AgMk$Q!j~I#+>0YnkDvz1BMs%?-;W}6_l7l1wr(@&T&fum$!0HkalY1TuuqP{Lb2P zAmM{$@PvIQ<;v-lZM*~?|5L^5@5kL+Hq;L}PK&4Z4dp;@y@wz0XO=UoNY@3afig1} z5XHXeU3A@<9f~DjNVd?aJk}Rg5K1b8D!gbB4JF{zok>;AppL3&gGB-tz^IhZh&Yrh25iB(Rnd6n$MUQ^>p4Zt`4M9dV zr5wl*o9+F2)AaHaB-bBJN{wnP8W3aAnA8{!cJj}P@p}`d7|+(nps+4!-EWHhn_98l zb8vNr3M})Hb~B*q5pkF`x=Nmx#UQgt70IC(B1-ArxV*4Ib~UFI^=0E5)#LLTH7J(@ zGmM$jTf1~bGMK=sQ=K#oZMcxcc^uMylA16&)!dYrVx^wALi1Jc+{QTPsnwT`SA{@d zWb&1FTtQ=HWa!WmoNG1qvD~L%NaBg;Hm8LZSt1X*%N3>Tulc2b?w-;nf&_5G-fiIW zdiIuS>kFJ=G?gP+7%mlUYbui~VG1cMT%bBzTFeP(sU zLH|LWR3Ag|te6g#fZcj`>5f6(MG+#AZ3|4C!6>o~P0FBL;NNTZ`mM;Sh(^%#MFT~7xiE+_lLf^?Gg<2*1ZR7deQ z#>AsoZ=&{sa!acoG#*?TD6QQjd(-G50o&Jg z+&MX1zRrIuG(fDWh`SvKO0wI%Q2H3)oa8_d4{H0^%5$~Ob^BzpDQfJ+!w~F@G8*%3 ziM7t~<8~rtr;Y2*@P&f?+)j>kE3gdZGn)WA5t?lhtLgO%Lu`CJ)Y8@#b>fmhPcnIQ zzn`WVxX`FjIKBN34MV7l7cc`NdyJRCboerXHIihwm#@vjFKIcdh^6`4&{Wj*hu~Q| zpFL_gSaz6^ND;*#xLUSeyb-#}NFCfW(r(7;87flkXD`LhA1b;SKI$ORW3}%k=wgTK zM7Kqp*UW6fdj*~IJDPSkdLYaox5zIfZZ?!ub z)~}rK5^E2}_UF7lYANNPE}(f@RRRQO6&}J^eQe1faP8wo>A83&U&n_|2CdH#{ z5x)T$AZi<;7LQOp$aJc}7TncZ7dw?IIOU`(%rT_Z% zj-_(MJ4bn_%|bri|nx#zHK?T#(ZsL7kH)p3I$E2 z&=9w?CstFR_1lcSnTAc>30~cYZqr>XEK*8zwiHlc8=K8Y`%<*GUu_GmH{Ms8u!w2v}>~3 za>Wl{#O|wp^K9l7uk~ciqW;}wJmP#I@HGm$$|EZ{4bY8GXlPL!UOZOgQIqkq*Vk8b zcO~Xb$O;2NFwpl;PdP6uZD#7Z50*lyh(%bQiCnH{e?fwoaBAr8(_XS|>8HLR7WM1v z=DV{y7HsT{XX6pDdkL@vPfXEO7!Hi3;PhfBG3ZkSz}2Xy1PsWE@LR2N)nFBnX{!s` zz1C#?JMOelOgcKeZ;#SL0{^3N5?Ir|>VYvF@pWMQyPM-vYVT|NhW}_S6AZaCBZS4W ze|Yu~_NI)|=Wi+z3KO_7&2Reh`Wo~f@`&DchEPZKIK>lF$AbR5_x>En?Ga6Oa2Ix@ zoX>xMT71cS|5&pL3=uc(iDM(i`i~dx&OLZgOyF<6t`|K04@F&%>HD)G)+4V~K+ZIO zRr`sYl|;M1U^!-Xdsvx$; z1=5Yt9nQMz6`t10OP1&7_0d;; z3*dftZiR!QOd3`W6h-}48=N^5v{~id$i2dX7&4E`46G@IRNRk;j7?P$Nc${Am%kv{ zX>0QJDc|=u?CDWj`Nv5jVPUS<-wwQIvoDz%+_GASk1ChmX+Y9??|ngqV{s7T{*lE)W$N9h!#aGdV!sRYum)6 zEPCYB_x)DM>q1A419@5$|Blf_+r+$CsrANUL`EWc--53~noN6AA8q1(Qat6IR0I__ z$x9!vGhBt$Y0labaU=|BU)o$cX$e@GtPJJO#!Qgac|Vg5ATwS4VuC5N_5|4`?R`iL zTgf{Ho5LiMgDj%R8aR-QNQ`SauRnBe(f*um5IQJWlv8G=e0S?1eaz(&@I7&o2hL8x zB^i+OT>9NJIZ>OwkHuH)DdfLjbYwl|!syiJW31(P_LhvKnVGuo?Yj{QxHtuB{kN}p%QUyvjL@Jp)s+AOGE*e6X!OUcu!EhVQBb(wZ6}2JKB0$k z<=FF~#IRSSw8#~WNl);diO0_+=*&J~>kKI`aIEYk68{qKLIgWv=xPl*+4#fs;t{&k zK=%bn&vTDzVeGjhPX4VyVjpgDk~s#t)B7267F)U6L_M8O0|e;aCuXc(fYJt^!yH#u zh&wv2{0W*to72hxHDS@Km$Sv`p~SG3kc^v3#oUg-9mvX4xu-dySXMYhOvd204l2im zkBL&B=p;eaHb+vLj}8{wRqJJN(sZV3tWE$8p;@NY$ufkAzE?xL(;O?m+$&}4UnV&_XQH-dp-jz^PkD=zc+%Hd z@%|Qz%e0+ji3BDb`QAaCUIPVfQWgN1ijwaWS|IL#JY+<&rwj`aK4g%ik@?vd#a{=B zh`KJ$z#1JM5_q{fM!!v3?7II!Tqd`Ce%f2^o6W6}uN0?A!1uQy%z3fjesNa*RNfMR zNtpf&;V~SJTP>6Sh9St|f-<6)^r;NSaBSl#S8Q2YxmL5F0FeZDmC)Sa<{Fwsu=( zM=B)J((!8ezp;8#NcuF=$_*<1CDer&M)so;p8fmyqk@u}!C6o3O}#9yl20y^BxptO z@W{*LfeN28$E~CTd!{1j?P(Xo;QQBKLGOjKEW2)b5CkJbG%tn4huZyj&;S&H(9sn0 z2oh^H*n5AFuxiBk6Oy6~4JH*&ZLcel@+j_4Kk+TXqJ1szft6t;HemTkkCkrnk>K=F zlyu$ouYHe6v}!(pju`{vM`*&dvSbAR8UOvYaQrbYhab7W~87= ziPM4$tlL-H_}QD0kYF*8)E`zU8HDowbXNBhe|1*CWIQ1e*R6jwI$lL(IlCerq#7du zvi@yIzdknC%h4ZGt>~ke++6o6zh!cL`n5Z?HfuHNix;)QV5Y5yTAQ`cx!zd?L6{{$ ziOgliQ@-{>WLw4^s{7M?=+ZLq>=`tu{zn4A3rL18l^cGgt`YJu^9{a7(_`o@R_?ox5G`b-Px;1>tHKGI5OHilUq51(ySrp)coK>^|SJOjT z{n!IV)RkMeVP<57p&-iNwL}vIZS?DLUX^yD$Ej%Ul+Rj^W1#?BOfaVNXB$S*r(2ED zR0@g3xm*!TJ7+6(8x1}DejpgcO%>;D^BbhQmPXJ%<3mKp)NDv`54d8d>d;xCx{MW6d zFC;Kp>3dJ@UZ$v=|7g}<{pCX#KNL#;w5a)SWi_p~G+4uxK;hA=t)4mZUC%!b zc*6Xus#c~rGt>;cb;p(dI+eJ9`0LtJ31kO_&U7NFIj)|LQ#H@I@^YoLrHN(bb@U8O z8jtY`;s1U<9DyDck|9?!jvght=x0%;WaHtwA=*wPWYH zUBG`mV5{*mT6K^Gg$g^kfbgTr1{!pINFF0~fG})^`5~H+D;{&c|2btwYb+oi|Exf|Ula>1G6qDtcbyW5FmDtwYOKPjAm(=rn(r(^67;e!lbIOh;kL7_kYtg8cl_?t#Nf3%Br|)|P?&)aXC>Hg%-Z6_Eixt`WM*5aw6N&3c zsnkQWKypkP*XsrSmnX&EsBJ(*;FN2$HuQYHlyeagan_Ktk2f8A|UUi8# zm*3LaGFHHO=t+S1d|8+QeSc(aku9c^-^_$0l+idKVnIww_G>$Jg_js-eJ-)H`m|ES z`>Uadx2B@If;rq(!}=)@;~0F^hn$D=c!TFE5Y9!V_KRJ?SsMCP&M{0({?~&U+yo=5 zcR+g~)O*=#dRwE$BE*hG#Xmwe+VWrfd<#h%?JE$+q;#9`AUB=(qF8a<7+&FIO1@|P z>6@kv(7d<<*o$sOVtP$u4op4FlBE8yx|h8wnD(40oZLaYD>zdlJx`p;S}qLCRF?y| zDq)xBbRq(c=qHV@0R;%ISJ@ViGs$aM#qh@4#?bfZ++9BNiU;DH;GdVcC-nPl`_GkV z3Ypw(i%5NKhb*{&F|y+BA`yEC!=hKe{?OVB5%_^%9jV5qF0dsQF7`E3lbe3#VD+h^ zhqtbIKHPP(~T&dGIdFcDaYZp$=-NtUphOS*H0bEb2a^RJ_z_msFHkN!%ndtKF+eVHvn z`p$_?gjEOb;gr)Eoa(AA$Rx1SGrtplcsP~3ocoRUSE#LZ49Q&ET=Dn)QR(~UktZbk zwEOfNjej;Z=Z)trQ<&ZpNeXbG&7o1RNc(fXTn)$AP%}1s5(H+-Jbx}Kb}&p99=TBh zjFZDrnZROpwh)WEb_ZTN064>63~pY(q=AFs&H3t2p8Y!px82_Gd*D6YomUt>ndb-wsQ^(g*{#-Ne8X7F8^QCX##NO)Sk4pbZaDhqS_5HR}d5 zTy;E25swktmoiXEm8~M~d*F*~M#x-@!l#`lE!DK6UI42BPXOg}v|y$DY$26}pwUh{ zx#bE@-<80hva8FXlVnP){w~FR60iCTx+Sm+S>6_HO6g=RDhfr4lOK2x^1@ zKiUBKw^ZgOC4PyL+ACSN%L%jP#LnF)<~4;lOf_&m;Ci|Tr+D{%uGkdyn!Sr7vE`Hh z8BWe>ri*A0y8Qj(@ONc@m+?2{=)%aaSs<2&Ut{W=H*xGmi?^5ooU06wzN<k*5S2g_vRw>wvM~Q81YAU~LjPKK5e?W)?4*@HSB3s7; zn6;Xr0Jge2N|pM%R6(Vmsm~SDW;5tSMkj&zS++u(Dv4X^=iBRoufOWPYh;7E!K-4< z$Be{heubsE#VE!#Fa9=&hDZ#>JRF6*_vCJR`KaI+EVDdU+8cwTB&FN`A<=GQ=WsdM zaz)gFc`~nMw=!F(c#3iQqQ>f1BcBAwG5kn{!ak*rR_Gpv9s#1gQRUkb-JU?}< zN^>r9-r@kEF-sPW;-zYQlb13~1`)o^!Zf+)ObWyhn@RvyGQd#0gW{G(Hg2us3k2*4 z-2?I^>RetnEXdWl99Gox0vuzLY`;~%F~MDT}RXD+7~91 zM|arUJtjaeTA&#?UV5{a%OrZChJp|75lw)M^egr2AGRnP!pR_6sUPl(mbdY!Iw3eW zTkDz81c3BYDXL+%Vhe8jy3%=q|gSHUpfc=?C;L`(6;nj?OTy0 zFF-IStNAVM{8pasiV8@^pTlijlJ3O(YIXy@Q2n|jXMT-8DqcnjXb-Xx z)Uvj*v=7XF^d4Vh(g7Dd05~~xsB@UqGAJ=BP_&xG8-vb!Uqygx-*eyR{y`G&9ZYgC zrQiMQT`gy-Ao>JT-GfA^ctwT9FvG=qcH}F(ayrsJ)HT1Owu=czCGZ1ZB~C4jX%`64 zcrlE$RgHxrJ4_BOe@d=rb`)ZjsyUkSHD~*w^Chaqc!#1I&OA?MhEkM%%k#yV^pln zX#VuP9Mh%ChDW+g12herDS!<~^Wxp_sOQE%yC%%i*=)ZbNYC@hYl-CM33oqj_XW-C z;%1ovT({yq)Ay+9<&D{I%6$fTr|taiS&S9$|9oi?>Dl~KWI9=8v73NP-d!n)gRUZ8 zPU)l0VfbWm#&s_CzEWK#Ok{WwlXS}TLbioD9jH0%mdg&q2RSi znMNG-Ryb|E;+3ngn&3KnEWW-HuU`|WUQH}W9AN1J(384>3+#K1XL<(BU>~OrV$Iqr zzPyKHd;pKh-$+pWc}69>-uLlvz7+4ejPDpOp4~Ru)OD;rN=yVilz^42!wyT=|rz4rUq~r*6N5Ng6C;22dlQM68-6PU+>8 zSI?iQ2W3*lzjb*d_M}5}6q_cf5FpSg&bn_ER~b808yWj>VWlWsbp!c$V0bb~nUIt5 zwUUy$7t0bJF>i`t6q4cX1p_D=h=+7YyL-MViT1mPHE9Kr#5PG7Sx?69^rff++&* z4p)Om(=AA^pB%E+S?%_hJwwWtM;cbzLU|v0*uHfY=BI6nEbV=LQ~An`^_)H+IS;RB zY1L^ZRWg=dNH3kNoivYK{zNX*kfP94KkJFZLhb>P znmLfQhtSW^<_WZ@8aH;|7YYd`KRDF9DJJ$JMFF2sIB{sc5-7uodR?D$N2^2Z?CKIC zKno36l>q6JoDxMmt>7$>+RNPGSMOmWSm&EfDv=Tn3A>Z$6bX!X_JeWXh2-PDj+!^L z=f0S8@PV{NOcfBZY<`-j>A?PKjB%G_Pf+HosUmrr$kP@Ve{lW`&G|8v7e|- zE#~Dp;}Q|5o_GT^-+OW}*0R|kg2ry3MAbSXpC{^PT%hBaF&FV7^h zTwy_8J86J6*6t~)++;dvh5?r=#x=dzAHfAe8b&B1Z!htWq&>BhOx9aOh8jZ^^PQqS z@u;5S(h1XYJCq%784kzdPZ5Mtscj(rLPrYS7RKoj*calcyt)r8 zI#orHgOm{LoR8UQoQA$v)N~@ZG07ZE>i!pW5~_Wayb<*BaXDBRDXga-gM-R0uh#I@ zktkCUU7gB%IPCM^4uG}pF;hQ@W+H{T){Li}kR(a>C{J~^*&}^W>qW+llYX9Rt(-ug4;gT!wIJae@e-2r-djtD&SBrMwt8|`BV7G3IB~qMi)LnsxeV93@$e z-&OD#A=|)dKmKLG)0{-J$`!AxWR?gTbJS##tehsBMd*FC$=>I9$RxcozMnx3EcTEG ztjO)~ApzBl^lYX;5ZFtFyX7wEpj9NNLy?ddj`$hmUoz%b9pluEY48+v*j>v^_S*_8 zlIv}L8RGgl5kds3tJ2g;91j3lTf?tELfcBE!Z)ys8k8CG(~EIg9(%k0+WTg>URPMF zBDNHot*|Z=IzxrAN%SYqC8m3>5xyakK}#$;!kJ-d;9YpXm~!#QGWq!EubexVTy6LEcM50t5x7)Lv#>tO zayn7>nIF;`p!r%C5Q$7NbA}3w=y)*K?0QksesBuup0l}1=pca!{3*PGmgbbdv8yLt zsp!;Xao1C|E*fiz0Bx*?P^Bx5pLdRXJ{qAPK7kZ{rph1In(XHSbRr7TPTviaKwBYC zH^$BWevbFUesM!w|GK3-D}6yQJ<^m2X84F_(T`g5y!G{Oh|2RV(8?1#2U#^1CZt2& z=VS6wo|BjF%Z}OHH(7+6MSN~@?{N-`V)wEyFqR675U`rg7%Sw0NKm^ECK+M63!xg}#p6Ml{i(1lQfXsp~FT|E%=`|AGtzBe&)KkZUUb-)E$J_NA z1I~+->u8&G^MemjJiA0YnP?Y?;(mraCcbv}{LvyJ=<*En)v?W&2v_< zrKC)yTG4^ILGg2KA=rqI$Ucyo^ZGm{w{}q<5o@~TYqL? zoNFo#U`bI@iIqhPjSo^YL__A6{L|O$Uhpey>Ha`E9>W9nn>h@gY3os(9HoD4BO#3# z)u)DB**P9Zwpy}WAI|64RSw-i&1-JEI=!o!p(to(rLFBedz`=@>QRvpf$oCC3d1d` zKk=O46`Vf^G!;^F)GG9$qN8fJD>`RDHER>sj>2Pz{jD4mLN*!_l;t%_1_$m)-Tq(A zUXPloP7!T~{c%4_P?x=3ttSya!>m8|XQZe*s$O310YSiyWVMpznx#EZGHpXqvpi4eiRYzM8Py-R0C+kH8*neImfu&+_SMdi+7w$Zo)+ZItEh%K9ao1{& zy2x zNrPnT7tOaH5SslYR|5>B2W?A>Jk-5gz+Ml>=B^XOKwiQpZD7ySCW)K6TVl``!4#R1 zlP);0+R!oSe7540_Z{JxG~peJaSUPYx82-_D(md#I%zKbX*GDn!6X&HBh_p+A)O?^((x1Q%+1Phnyu}vav27R$GCoi-pz}jXF z8R>fEH!|)CQ~~_u0>DAbsOV7U7^D0)fe>QBlhdS(M43SO=gHl|P@Q0<|6>x{lWD3Pn*b=$Tc^GHYi zcU#DX(O=R84HSd)v1;Hr!GkrQqgFS1)pMqYu(}FOtkLZajb!aN^#?(RB^cKKq72=e zqN0(s0UqsPb;c{S)3tsPXX|18@)EWf@_*5We8Eik0+if*C$}X(eWXe-b{aLmn%bq$ z-x3=CS0jgQ9^PXK?+WLdTMEnAJ#)NFrp#aUsBCB+dvqLj{);*U=tzETR;Qz9o#F8` zM{{(BlJHf}z-rCX0mFF`yAStD%YQvcEFS4ZFf4?KI^Ib% zPV&k);lC+QOi1ipdFn#Tk&Ut%t+eQHt}38#gSXA$dAiAbA0SQAZJL@JPhB-31|8RF zEUX6m6<=wtJ8Ap;oBD*Ek+uF*7{73i#iZhS{Ts5)OWFRKgmk5=n)H82F%VfYavrOw zKSi27f_vZTJaY~_jVqyT?Mpo^{har=v<@UkXl2DoeA>6-G`?!l%Ez-iKY_qxz61L_B7pq} zf}kM{7Sl(tj%Ws7X?Y6A+#5XiUr9e|`_c49P ztJh!%;7I5tNyjVyLu3KiT@adf6hktro6D05n;C&O4KBr~S&%8vsg9LitmGdonkWX} zkT+&bm{xxGi9A{G;u^52(#iYBZD+#}2%N)4#`!i_Jr@LqiuD$fW(@@cmh?V7_B2!3 zr*Pb-tW}?w5FmS%OQd5Ql`DUIP57D>3FDU>z>sXVoo3p4ejE=@|1oi26IACMwap#g zTpKx~6{hwpRT+|0kx(@7F3)kM*nt9RBpEopBByl*=!=QKj;IWoH21H0i6f60X?BBi zTT58{4(UT_;HfVZTf+*_7eEj`$ge^^p>&@SGqHLAMOSGnYc;rU=2HfcIr?w(X8uci z!Cwer$z$C;NAJ3uvKhNi6*YTeR?=nE+5UqW_C*|Itw&#JrW8M;l*Bakl6eqpelvvw zJYo1RuzwK)6}y`!ZGTvloUmDb_?XW!EyaB`XPqE}t+Z$S`RD)n=&wn}oP|#lhsW-a zG-8Jff7uF&7!YA}XTCC!NDbJ5{1vwV8ztu51UeKbEB%|R2TZzuiw%^=?=v%nTBoxN zZl)W}CJT$+zZ`)VgHUc!yui0@mr<|8fqa}5JSe$(i>DsXT2SD=h=+cSaUFQVoC`1j zfu^9m4@H4@uu*P*T*kbD{qsE01LH|NML>bea{PHe?#l-|5c{@4Y5)cuDiNv~U!X8n z>S#~6(Sd|F{kl_tNP|>O)oY8>kzWsC6J?aNN~HS=KuVBw9{$xfnEW3(Q^q5JYyQij zCv5g=#NYxD&{1EYStG&c_mwM-(QoZQ0s}~(&?;FmG5{hSc3j5QX8s+q_TZiNdyO<{ z%@OkcllI{&PfY;k%P*2Gokd{R-F;6OYlw&d2``L1<)>9Z^be;97TaBnfGbb+|5FU5 z{}KZ*u=m;_nhIW8Js{ESY45R#u4GhZ4cc(+$6uBOdQef;L~rx2{u>F}PgBex>Jgi2 z=`0D<*ZYiz1K0FE2rK}=c>hECh-BJu#va$^Gw;KerBKwX3f14`fcV{kK-d4{oA0`- zU3=^q!#;PWy`>tD$GxxU7vlX7QFUeS*{3qj2LIWm#o7e>yVG?YTi3tgaF7Q7^FBfVs|I8hpIPc#-)^yukaprYna@Q2t)0bM$M|<2U zdZS3?xtiKyZ}@+G(5UY4=hcei-HAT%9%ci4{>C$2*D)Wl(W7tgyS_0tiWyP;f4=u& zH5|91bJlaYb846LV%`1d*ILioT{B15$Ob;<=^KFx5U{(W_wXYJLbas9Q^|l@mK|UR zZwJxZ93KZHMq-{4y(+vP^Vjr9p3>O(wcZ#{xN^)+a!DL>k7#s(_Fn?3KY)c=(svwM zEK@djFZgR$fv|a{WP;-tZ_|bKW9M%4-Ct!s&bT&`5buMFyoR(GRQ$)wH8+cz%^cbw z3%9weTl|%#I$N~UZzK<4Paj+d@tv0mVAH&E{`+NG;LD#TUgY=KFe=9EBmsfnakP-& z4M3B%`Ui6)sQ%b!J>|H@;43S#u-)~$Alx?nucSArd$%;!wCz;F9zEYlG|&%!5x3Lb z%thA3mU``wJN!h~#5|2pBHu&k3hdM;Pk&e=vY289296R7Ky)4%+Q65c$iFOOlKAzC0sV+AS*ZZAQzl0t2FjFv;T_F^UQN?o1XW#W_?h0oiQE9S&d zr};OMq!YA5$yUI($7zwPMW%X!O9cEk*G0N%p&;^vZp}=TM8dA)j;V%nH9PAG3X5c+p{Yy44=IZ~=M{ z;2uPqg5UmXq^`wBscBJzk*sE7wbCXAr&G^mMws23?P#xIZzpdy@uH&x>i?itS@!)2B}1T7riCtI1~uf>w_Ed@GG7=!5f z=2i0*6gu2Dn9{K^`HprycGn~mS2Mv(Uy5%J&!~0<=D$aamf-dgPfc^-m6n7u8EmeC2k1VLOuwDqmX$s)%h<&g9>Or;6SU%-uEv2Cn{gDcvRnK(Pok z3n~Vu7+yb=ZkEu=LNv1}Q+Px)1n@D6&gJX9VSVc|MVBuS6%qb-xlo6sddThADW7X; zljY>WyX=Ym{~3<*dCgl6@5;}P5A9Y_BPdq=QKeDm_ZRFACP*IVf_Og&uDrYmnbN&D z-^%~!2=ke`FohW|ORo$(?8!qM{c~Ga-0Q86mdAe7HpygfUM(Q6^X5|1 z(y?i({`e{9MRHdwE!iwd1~k9@NOHPi4$tE{6|_Slwgl(olXs=4a3r) z+$i0wJ|hGq6SM~A25vtT03*2`q(O@ny?17>OA-@k%9^HACA;f&Y=9tV(6oZ8-0e6L-J7z>N z#Xs&rs2E&+|0#){?|a*z^A+#)me$<>9JtY0C%Qi>nm_LUqwTGus_xdd(Z!-0B&9>8 zBt+?Mq)S0UKt#Gb7M;>4A>E<0G}0glN=qXxCEb1I65Y>!zrDvh&iKYT|2c56_~o4U zece~Mk9HkAFrUNZAfhK=Q%Fx5kXhZ8pNL1u$6bjs+7t#lrtbJq_su4m(h-e`?EX(1 zNO!BB4Tj)j^J?*0dOk%MahUZVn&eIw+!N~;n-WDH*mjwC^0RU0xwbYMwXTbhK4s>< zR6dnWX%zAp;@EOYu)e}nq^Ipxx6#)Ac5VRT`eM`$gh;g5s_M|gr~2%>jf*pE&;TV{7cx(*vpdDUF9g z#lx!nv6+>Tk31i6#I3whTyE#4a9evngvF{UazUv1@gn<2Oag5E^<7u=E;SPRZf9B4S(WZXp zomj-jTwW>OoJ$B*1zesKmoE`pA3vkaqsx12-Eq4qLQgp}GMxfUMhw<0j`{c>UmIFw zi?*Ky{wl?~d1f#>JFPvJEp8j}5GtV0JucH7B8Ki&DGe@!Wi!TPDUoMCO>{@gPGl-8 z+oVx|7_PU)7q3>v=T&9hPZw-!>HbkIa4{Y6Xim>MakPVQ^g+9lz11_oSY&o}gxw2? zv~2B%oJy#Kr`tZ86NJ1d<=9)n3|=A54?}d?F|>LT0>L0)fQVpmkWHG6jh@hUeAgAA zXLN@#KY}~jT4N0Xmdn&J4sQ^3tim{NXA}Fb) zN$$@^t-{IYh-qDL@|~3XiQ0Ys8N@lQVh6#^X_nN7V?MeR1W#OuoK*WbNV4`KCviLx za)zA^hc)9F5#B0O8A^8I8uEW2MaXF#uq_~E4Rx36k0tATsBRE1j-*neZcuC|t&`u< z))9&vW|*arn)UHK4yfiJtpkaBZEupb%9C3AC4W~a;*erb$s@|pPTb45jN#j7zlbLm z-XHiq3Qy7CM;O^J$a`uxOhLIgd|X=%S))KwNuUzSU3p&YxP5C{pZU*yL5`G6t)3x~ zZd!9dZa)ZdYWcwZOBh*+Sh;!iygRESgx{1LDHXSyoDL!Tma~V}yB2^1KOwv)dS4aF z2){KW_Z9*!@(Vcb2I1!djYNwqMaS120%dHZVwpkr2oVA_ziL7{AIMoSHZKYzX1T?b zDULA8Lv2DMbot^|?|0!sy4xGO_ zcSTh-#9Qx#QM6W&)_Q5>I9cCR^eWc zJe$+q$sy}WcRUT(fMMW^lHUGxu`;83#XIVa)x_mUV_qmsj6V^oyy zJSZ<>J(-_D=FfiB_4{s-UT_%fJX8hsM>1$Bk+s&421<`&>hBTV|2D)?raaZHNHVc9 z$8To2OBJ~ND#|K%_(}&cSAwbC=%AV&-V}bvN@0>_u24K&?T-9?$_Aesl^gPV9C!J6 zA1KTk-f82%gLxE1;gS~S)LL%4M|!(kBM&@T+3Z&@!V7wjL@sl~{09S1Ie|IFK3Y3# zEYDu$2=ukqqn(`HH%rrDHjLVkOc;%1jI!{y`EVeH6T)Mk>++1e5>|}@2o&w9aJ8YM z`MiwJ35mzIhoNuY^e54Qb|OlxD_amr+{D~M1(+VK((jRBzU-y@(((HiGY3#{4P}Tk zyalM3$O3v<-{nhFxjS5sFEh_&Ni4UA^niubICFT?`b%mL%kV#Kt(p>WBPiCBWF|nP zQJ{Tz^6unj5egj2jlwb?ucuOZ9fJ*@Ky$#z0T%Nhf|K#(+x&B%pb^5UIe)+NMF4Ta zJLYKntZe5xB#}2qGWji}7Yg@p_Q7(;iF!7b#g-6N>Zqh3?#xF;-W}>M8*`iLxUffW zV{ApC{^_NAzKq7De?$epAIu{1hViCw%qolt;zk(ZXxYNdf--7PJH}2DOb$`Zu)OpZ z{CoW79N)Q$%siPHd8dYTb1u!ugj$i`8C|k{5(v*0>A30Gi4?t{1?g|KqlMY$W zVR;;77T*f9EQ-EPE0->eaD9Cr=jLqz&3d#ad?{h{d3{d=Vd;gZ+QxOI>~J)HAzy96 z0iRm&}rFmcxUK;pc8}eQ86jZ`}QR9Dus0K4%wP@ROs#hz!|cbL?OO?Lb>{S z{*OF1(5u2Nqw6Ky;~ke|{sY}$PJ$vwXE$J(TM~#y_~(GB*BTr@0SL%4>IY_{TvI(& zK$3co{Tt;dFa?6GncP`B9TGK5(gChJihn*F877nV{GJT9@E~h`X(_ZN_S_ZoViQ-8 zL^l1U5~kh2J_d&xlWt{%m8U2do=-qIlxY z%>ccrc{xSt^?U@AgUQF9K@JjVH})%GiG22g8yOrX`|oAxze0nitlXvM>v#MtEEy%Wk~ z-0Y81J9gh-mZ$krPxE;-Q7|miVBbfpJtWTixpwKsb|Q zy-&mQ;-*e{*oVIIbF6EWc3qI;wZqR23*8^uI74Y3yVxa>)y1M(0@+HgtjPH-f6y?E zK*;1LxmW!uYz?W8RI4r2vXBQVJo4jJA#Usv5niok%Z4|V8&kZ&o9` z4)xv@X7ucP0<0P~KgnCaO*^MrqTou)5Wnc*s`=_{W+HU-{$iD3szEgzpVg!RhKFA; zMjJe4Limc=r~X%U@WmSYOiCi(qTdJOuWKayc82+zD(oZkxT)97B+uPEw0{(!M*6w} z$i`F?cWECyEuX&|YYj6M9sqy1v3(!X$>!~OYmXHXmpJwQy0%=Yq?pCNH#*xM(w~)U zr_p&oeTJO3z#L{}1zeVRw!X^i3hnTy3*d`~z@fuJ6okt4OtXr`Py*wI81U9#u^7$1&!l1O-9(tJ~wiSu;q zV7l)u71##v2?3$F!S|AMUYx2MGhG$+`eW8%ND{_~L#Mjti6yd1$x7y-)HKmTsipSa zWJr=-Ka@u?;jtt@8t?tti_J#2pamaB9$6CcVN@7B(A_EWMcs$iQU*6oF$(?87x z794p@j{Nd78X-?Nnz(DuAqBcR;Onnm&K9qm*TRX8+}vmAw6mcK+%TM}l5j_N%Z*Q7 z3b3c(_B5>Lp+e6A9!zqx0tv1?Dl2yi$gg|u9{dXx7G>e_;e6-cN?y{xuvxce^AR>r z@rz@YGg849=6E{(R?gb8t+H58qpHQ}6+A@=X@u{QAP@&(Ap2ZzdM#~q%?5l&SmFmu z0GDSq{81LYf9pjoKoPuaJ8?F>z(uDEh<*v&^Wz7ZP2Id%VpQ6n67KD4FQBqgk9e!VK$%kLr1vvg1b?0 zS6*Wv3-*XCPc{r6nF(D@I`wOO0z?yWynQN<<1eI0n_-eHKK4SmAs;}i4z)5H++VNU zn4Q^R|K$ZJ|HL;?y1qG?cIUT()}gU22glf}_Ji^hFWPe!#NXT8!V6z@VT+-v!YYe7 z*IhEKb7rjb0xn63bT64&qiCF{^u=8pK@6NOt3Z@dT=B`7o z$b@}WGuUA3xph|80}?F9k2!A6q1K1dN8*qKq2pTr{Or2^dOmv76iIjy_T*~8UsKoH zreL5&wx^{YWo0C0G@UI8fbG`Wvd6T|v)dIP6q**Q!*PD3Gy5^N5K>0U2zPHsqLSq0 z;(kcL%`4!*4oYY_J5^{nEU$DlgrY6EQ#rRvB) z2D?m-*3A(^SBr-Yw2t%b%CO$AvA}RjV(MuXhq9vDc)ouTrtNx)akDVxO6RxCh3wY4 zi>4Y^o(>U2L4^TzSK7_>hEvc>lrdIfsd!`k-csTdiY4O<4eVHEd)4?M7RBq zPq(YWRhoe+k%QEKMr&Xc$LI&2gQ5P9F`NoBM|ddCw-!_Ial;mo7Uj1_lfA%3<9`nO zZtOhX%Yj)W3wwVu(|KeX;}nk`t9raQooW?ujOb#pE;=ZQw={XCSWm%5p z0KRJA-~apX`yww?hL;Sq1=u$eW;FvTdBW1Bv-fZZ zO2Bs~2?Sqy^CbN~Jiyj4{h<1fHwh?zv`<5RO>tLzbox84bbSJj?C<6Jx4Cc@Gm;9z zZwc+qFdL96z>M&>9u(<@eh;ROk8k}RLf$x^+>9E*EsehO|ILS`h6*ig*1RPfu}Y}# zTzWX6|0>n^=MeB6sRw9SfvA8qD*m(k+So^R8mrvDLufedK_}u-xHW-rwoXoNCy6-g z(4(BE?oQ}@B|yD#b8iSm1MRy9xXXv{_`4hEFs!-BHah0NgCRJyuGFJPiCFZLH#%n!P=faF=69mJ#^PI|24H|1mp{WA=H= z)|lh$>Y(j8kO&%A9a`U^f1bVnHu-u#hfgDspLUtkRDM?xjSH9h4s41~W%3U(M8h8v zTcs@9e)r?@+994G%hDeDo0q}n_@c!$!UalXNyCV_iin_x1kBRflp|ChKiVx zqlcv2Cr)Tc7-!_`k%r=l|4_pWB$}xOU57 zIx+&+Jhr~kD(U2(n-eF!(!}Yi@d3@8|GkN=x58hJ2Sx^T?MJB+nY4Aptl90`>VWEi z2PWrg_Tc*d2P!u7Gk<`uo1cGWSmmT7tY~W2J`{?ncVBSduHYZZ@hro=O^oq ztK;Q>@X(5icZ`H`?ZGO86Sm^equORi+@)g2*fdn^7kH20e?}Vo86txR=ow^YDyp3J zwja>mn|WNo4pi2*OBbf=nz5`1q^;NAJJ=Lb7y8}1P<0djEfufC1^Or1_3$Tv6q61VF%wJ()WeyE%dC5y*`WIP(z{|+@pw90intiY4T-M3o;;fzeO67AT zB@1BZp#8F8S4sF#3QSl}zuEKvw1DZhh}S2QQMMc-ULH zD7T-G+Lk#!9SjH!um`I=HG9)ZmV@Iu1mTKW(ADmlhZqbU@4>1MrvD1!9q}ei$i1Nq_^zIpPlRuK?U12v5iC z^n=LqA=-`ZRpF!$#Md`A5YOMoam1+LfYEL1JIC!Gdc^F{Q>q(NfUD<8ibG#aKWxik z?_IyAwENL7Cb%gW;wCC>z`mH?W_jJh@kVWGQ}uk7R2cM}IR-$PljVhlg?Bc!)YTW) zky{OqPv7c5VqC$tf=?t0;C{sO`;8k@c{OTKv=2hKa_AX9=c|T3$nVtJ0~*S72a`|F zd}Xgl*5qML+3BLbitPJ(;4dc^o9Ie$&Re+)5PS_3^Fi)Ic+JwV z-PG{u_=kt@4_=cpb1*f=INuSRl07SHucnnxdYn8goN3R|E1di5iGEZ*nTS39U~ z;IoLQ(?GmoogN>0UH&aE4B8rdebObOwmi2D%&Vu_<@N8-P)d_Uqh#YWdK}{@=*@kO zR1Kwc9;c1NaSQ%VPETyAj)tSyFza>x1rW>#n0=yo@|=Z%y>c(`j!ZB==ZI(5X9H%$ zik%$+-)mG50$|!E)Wsj3kC8lg6$4(Xggj?{m#Yso^_-rxoEmTFsOq{c$FT_wO~Qm3 zx4Y@19G#wak=zw5dLzwWStB$Ch-(j=6tph;!R{M=C_~HJ8g@Km<|LNxS0LtIc^4WSLp+8JL$k^< zYe&Mn`r*(ovA3H*BG9nE`q&ccq7C;LWcetsC^D=Gmv zdqRbs2$`^Ft>=c$ti73S2>{JwjeFyf*K^0O0;~$gqP!01HQ7V4xrb>cyW$IO#v6*Dy%p!+OIOLD;(EGp{d)daMBWfb<^wAR$tIkk0*pO5y_ zucw8cWxA~rb^|*$%{pK(Pm#FS!i^+7$;s+2I4RnX?Tst&i^FAKEyl;)5;*Jky`%Y- z8BDdlj^3Ju^M!;F&#vGYnM9XH{_ET;qA&0jo}vnx-bhHMoNGz-1Tv*GGk9 z?r(Y|2dZ$i$9Q9`;2OA-Qa|%1f)GyynNl}nUwXo2N|l~bz&=i+$-NOg;nWi#EkI8P zko~WwSTY5Q5wlQ~9}yDeYi*9pT&MKg)aB8fpct z!G#n(XYy*Ql~&JFurtnrwXc%_H@dh8Bh6y%yTE2WUqE>}OvS~shUTp$^2*?V-`We6;3;`t@l(P_ZOLyUot#z%>CDe_WgtS9kk4?wim73dkb zKw2E?OAQ{;t-PYCU1RAf*rtWP0}rj`M@x*#QQ@8Oe;Bu<>13dp4?ic)> z*b9*YTej)?Zp%b2h`uU&Hl(~LJkO>LI9?m|KB1AF!?NTPX1+C-L!<2|(Bpry+pjMp zJ;2tqDBX~GIajuO8;T>26FF<6PCXURJitcimrqU;m_yL~TX20HT(a zSAS4HSucLl^1wo1X{d%{{Z;Ri%I#P7q8MJ^jP6MFb7>+FR$*sc;h`2Ziy#L-vaO$3 zr+yM;dFpk!p|89gwh_3Ebe8p+FhhJ=3AZotLHl9$exCrl6I&ATEWHTsFPWX1Ya==Z z4Z+8xU$D~ln8l9}XmbP_sfGQa;hH{3a z$H&K4)@OT{>eR5QFzD34jmCzeNuL0;21;x&5YuXp2Sgb9_1)|S*aR) z8LvR6Q81d*Wb}G63A4?yE#UuVoa)SNyMIhs;iqq4n{h(^^aoIdrppE6;|pkDOGi;1t(Fg0t~%*iFoOC(iUp&B2llS zmq&lWe~CDTzWILa!&@lhulg774f0c+9+{x; z<@WNlRwP@{Befz9`Onpnkj$FlkbB!t_+*^#17xo=(@&LOcQ%;HoiSQ77QXef)k6>9 zWNG$V5veG#p>b}2!KGGnQO?ZNWZyh~2 z#SBMZ3W^jrW3mx?VM*zEpsTvgk-3y@6-v(0(GRjdi1_4)P`nghoKQvLw4d4*Xh#9z zd`HZ9e#JqG2HMZznl>MruPzu0ff9*35JMamOY)G{i@U!2RZhF;A7n~Sv)F2%JIi(T zw_^|yAz#(45|T2;9Pf+C_0L_%Kj-lZOkmq5^8}ZJLqMP3LVbv59!moX|_9ccR zCS%;4PNd_U2B&otdD_00)f$OGj^@NvC(8@ZEF}F)=OCg2gW+pMqino^h{Q+`s*(Gh z=hyTCARkNtQUn`}1uGnB!Sk1FCUPW_*ud3Wjlb7JB}UcsGJ8EnxycSaqd=8dv6No= zsRpE@v(=1#=l1D~GonjuJ5c}^vwNTRDjAILBTY1Qbpph<=ksbmP?9IQ*BZ>V_?oGk z#FshbL3*fxsvfB$$}bRW1NHH@*2bK%RxZwhA4NO5^S}b__YJqr4qQYd|5*d-uvP?V zMc-wp^|}`pcqHk>$`2*ICZwGq>ziGif%7%FC>Z{Mm?S7e+A5lrZ}g5)vT5S(`0TCe z7_L?#b)@Nq1G{Gbf`rbhqz2T*k5c25I9j9}b z*sWlkxK}Lpzr7=!6O_)!9;>fw2_wI1C86BN9~b@3G15v~z?*RRG>amai1;;;Xvj?K zK{U2s9}#v>RcpQQ&1os%T0dWve`oz1??Ga0522@3=N;>DwDCSd-eLpu?nn-^0cRk5 zq=O=4IeUydRe_;=6A?1S!@feG+s({IGTqR>SRnEBQ4c4Z*EX!|Q;y2_DHDQcGR%7` zhMg@w<{>Xi!8K(Ge{m*$KizZjmMHIvVS7*=#8UBtSlyMn(NhgcWYoomLy)~9tO1#D zmi*zWF#Ebd7gVHX6Q)rL#@Tuo)d*x!%cyU47=0Fi;V6A$E)F{vS2k~I{ZTww_{j;q z1S={2`>Q_GU*xBNyb@ndSXQLWrzjYfFbM)uFv5dCE7v=uO2ntY zFPvv(Z8$$a65Hc~YHf$nVccj(v{Zr{KWAg{L>`d_2ko?z$;bAMwAcAoLn#Y^#fX8& z2)J-)Q}&xWs&nWRjX|c$x-Pmhmc#o}SI&=-LL}~Ih3H?Cnu*o9VqGLOU*vkEOuA^D zgc;+uopG(>qLMwiRv7*Ou|f-8siE!J-|rkT)&Oq>O^#IakTGUuf5xS)5SP~2YYa43 zL5xV@hxSXdn#z?7RPw<-`%ykF@fA56OBbDJsBJMJg9_}<3*xs+# zqDe#Qc~nWU%vfBeQUrn8`nY0NvI(Lcj4kc=i%>HTXLT8oNkw%j5us6) z!k9n`=>qK|k&CDV`z9c?D0DP^c%N#l&S6C(nXUxVhv)`WW4kM7)Wgh|oz7RJ`3)HpUC@Ow#PeD4dR`D522n_W4Ow z;rmL3VFL)^(XH>E1tSlXae|p_IO8r28Vm*p~{xPf2sJA&w0{pQY4;sYl zs1(h}5cJHS`_&$KykE5$!Y^`lvY455=RM`FP{*Vw-tDBQ$geMxk65K4%i7#~ex&Uo z<$!yaqq`xEph5A!(x#EbWw*dGo>^<3ai-7jZh5x9H1d$amuy;r4_g4}@8NY!7Y|Qvq;^V*I;6ea2M1voIipB1d z?h!eV9w11#f%PxpfF|MwCjECn5PYf^a|kr&lz#x8!Fi-;&pXG@0b)n= z=SQkUP}61lif!5ZUaO@@seJ<`HQmI;Lwhne$_*wF&?MMy$MJ&*Cq$b7YxbSA43L=A z)3}1kLXi$SA>T(i2dn3Kzs^T~o#)9Zj(e2uyioo`j=ddMvDV(wFtUSFb4LV+dNeZU z5`;~Kl(uz80_50xsTQqw?GlJ1xCF zV=w&h0rQtcN-v1;qJj5__kn+il}?p;T#D1^v#p;#89F}K@R$oAJhECJm!XD@bW*mU zk9*GFvGwm%L`jEriCB!@*QDCR78uRbuQZJT$zk0q!pD3rUvUI}(TqL!u$olFJk4P1 zK2J1?w0rQd$yBS!vkr@rUjvTzJgH|qf`o4po4ljUSkO$;;@ny}FQy(^g20sV%M_ZmwB9j;<0I-wZ+fL#IPfR198M@ddNe59R6P50!w*fruqJ10hp1a;b z{R`?I?P|+sbDo?rVKl&+bpHH%zB2XckK-l=_D?(F2G<~MhY8Ngh8Hbc`vA0JMnu7~ zUHM?;w{}neB8YFRz}?hq@-w_)dF0}!DEpvG#HB8 zoL4ps~Psk5A!V zG+Q$b4de*cv9)6Q&J)H%RTi2x*gQN=J|MbhdK#VuWLsOME%mj#Q#+4MUSOEYD!Bp+%EO#zKyNijg0)EMTRXPh)^a3wpB zPSy~vnn4iQ%eA50lBUJ3C>P^-L0jQ&%#~nwE3mL_eY&jvTD6G~DMd94V1nhZ5BnY6 zB(O3CQ=+zAvnMI7&@w~t{dH^_xfD&?1~b!4Exujnyitl}hpowPB?9*l-Z<{}Se56% zo@6bu^^ILp7hA)xSk>+TRb~XFS6>t7&sa**n<*F2#t;u-E2%DO*(?R9eo1!1paCeb z*auzx$T6EHLV!i><n!R8dR)i^CO!+AblV-p)B!Q)*R-CPN4mtsRaF*c+gW&1Xo5IT4 zr}|fW3F<44JWp&H=yp?w2l;)3gEp`DGD~UmaMmg*?V`l^PK0R{udgokQthnl@vZMX z|1+k8?)oHST7SNBzQ_u0A0)@J-g>GFna-dgybuKZKd)!+1`cZR=kaa5XMt}GD?qIr zk}@PH=NNYm;%8oCzN}dJM!HrWgc0?5HkqM&)TT1U)el(~XF|q1V?^SmC{7~~CI8(F zqu?F}wu#zDgAy^3=ByFY=UhOXsDamDjJ3!50j%--EoSAmDidAhS*?k@PLK1Z)L>R@+ ztQT=IvO2={2SEG!v`y5={#;W(eY2B_b0tYi`n(of#P-ToC4tSiKY{4m$Dw0I9BCl^ z>L9;T-%J&}Ck&SSix>O-F4<6K^O463n(65{!#F2W=BfEvD;X9Wz;f8CV!P=ag(|E| z79$05$Q+hEsRZI843U`S2Id6k2j;^$ir?8xStMs-HQ;VWIFDISecys?F6h%E(;MNv z=HE1GwfhSRebYjr*aX-H>~ur_dR?;o8w0f*lTS-`4E@ZZJBScdo4TivNTYwbt~p#C zh#P-wLp2tfEvv{z!aMuw(zH)$W@?J&*>8KfOWAu6a)r*2 zG8YM~q*jKjE%XMxA0F!6$MzKtPC;j9C~X+H{(-s;M*MQlJ*+HRC4| z?Z%zORUnzFAhhZN@QG++QAR~q?9k0PF@&5*FcGKSgx5mkgfE#ON&>RHagsfhtuUS8 zc!r*?N`j5|6tu5suSXIoF)2RgeH?xeZ@FR|stzi`yy}VaE-G?f&CYZvriDwf|19do z<|zivEV>TInSYhph~fDzXWgxPX!kqOXu>h*s&VK8PPXP6Le>Pl39;PfC}>sz7Rv6T zWNLw}z$OWqD75TMv&pM)xDAXy@XD;tUvEA%%n(jouV0Flhjt-U5=`Q$zi2+`WKWsv z?4E@K-IrLqqD=;3UZlH{EtRbD=oT`-=H~Kd;@S~`BC)8+;1wrvVY>6M+X+J<{oco6 znb$vj7VtzzcUkbvxQXw6#b5bLq(R#y58=W{hVBnKhU~XY3OP65(iB+YM9zDlb{b#k zyQ?Zb&hUOl>@B3n7z1nB;zEiWAt$5%1v4@oTI+(d_q`$-v}{rYB-974j} z>m_a{7>&e0Ryn`9LyX91vVk-$1SGbcqrT#J#u$EAzf43no`3VPVrJvuU$|a)UTd4# z@UD~AXNPwanCiT%R^C5OZ+UIsahB+vM`J2z?3eM_5|~k~VDAQe+9V9e9pd8V4ysO# zN=t~$nz#fs`W$)kml%Gd`Bh*UBKKebvvu&~P9q_OD(D1UC&Dlg)uEgUPS&ES*4fK@ z1iz$g&t5G3?MJZzVkGTzu?bQCcv!1FD4gmYB+m*M>e?n%Ga=?8Hwzb#80Ax6%%QT0 zs`#g0UY>RHcu7rxcpQKN6Axp{PJ#5=1mdW73%h1Z_`#4iYz!X3ix;0&c%_pMRL%4U zUV?L%bpf5~(=sgtLMK7d)B!g4NJA^s9nuGBx?*INlqsw|Qoj(OhCmP2B$^rrE`cAR zai9>&c37C_6jecpxX$9B*;0f;b8aM@>d2&$EhmNqN(#+83|E zD*{=G=xVv_8X6RVW7_eB4X8 z`R~liKXJ%laS-Lu^LYu6+696}l}W-|Rg~fy3`^DEIf&d)BNAOn{`(rMQB<_-McICr zAz#``pXlh{1-YZTm>pkM-K8Oz{qvuUanNO*glMw9M}J0$7?{1IwqfZr|~jO~R|`-+ruXn#-&b3S7~6{^0~ zeC@^X;zBqo5P9>|e4^gRP-6mesY!w@dENOUrCynYsuG4WoO<-bu$A$abiuBF*LHuw zSnOSSU9!sCUPxl5h9V2`G^1q$1w{9?Fc8FMLT(RT3|Qkm>1HURB*Mrz^8d|^QNhrF zoVNDI$!01wXn|t`B#b{bDl z^LG^&1n46#=B?l)(chNv;!I^MBAU$Lmj;AY)%ih>DJTB~E#upVmq%aMDyAL-D(OA2 zelY_26Q+U_Et}-5exQsF3+#CVn7JZkgwPKhiA{%K=HZWm z9cu$Rhr*g@#80gkV3y@#w)39olkv1o_|-pj}`dnhyjF)B4k&$+;k9 zyS(|33TJ&d{i<@>buq#hCLDL)hdHpoLc5mI7S zZNhO);4sDf8$HnSgqoJ}o7peqJYGM-nV=?^iLx{&cm{0MUV2Xh;vUbs#`|Td^iB*K zUVJn>H2j8A(w)2$-N_gzqkwxvkpy!Maf^{e~_!w-dC$YQhzox%c8Obvjes*>!Q<18p29@JPr?{gM~U4BJx>TZAt< zO|J9S1P-2kd{xIwn$w9*+&)n&b~g1Omjk#&wa=^m zPV06ui;1GzXzn2FlR7>y$#2#`2SrdT?Y4h3rqX{dxAh}c-RbT44O6JK&KX1r9oY`cc8@*h zNiQHJ6_V~pr6v%_W*=$|ea|>ho3ZV= zBCrQ$HOP^e^lu<-bw+a50A%o8e9Kb$X(*R(qv#{F>)dq4^TF-A(4uiM{>ja-^(--g*J*)2i&XN8s`co_hNY83%*MG^hRBDj=80f0v@1f)}5qV*v6P zjtfqHWc|D#Scy6y6k{gHeYG6kbF<`ESC6LlgFM9LAaZNlT_Rg8G&O1dR1_b2nF%-q zpzbD$MH^)l$#-uk^W^kwv(}*1Z=gO1JzjT;KfLUM5NLsVui=e#GE~eHy##d~#U!^2 zL?!9!2o46hXL_{<%c6Ko5bYyxtJkH5is&p+FGzS(G z{zom3rbY_Y^`@G@JIE%~Bm-LY#&as#qsPrMjF^WX#ia%uTh(8EQ+u>plf<*`wqBTD&pF6*Pcdm}XzcrUKGjz;-C2F98aUwH$$&7UZQLPB z9V)kj0`f^*1NsagtZaScH@FzmtF<*`PK^w2S!-olcg7lC{0SJg26DxS)-P%^CwbAQ zEz5BqaGy}zV=luM%WYEkABIy5W-5`S*&G|3b|6r6rL8WJE4#@M6ziOjW531Lh}qJF zyOtRDi%Jar?v8=i=DtYha~)n}w2+C9Ci%I;8xYI$Rlf0!kZLnaOGhQ?jo`#LVEyJ+ zEHl^Uy{+4&0PP-a<-fDZ!0mk?5YCyQT&Oj|4O`6&9@-O4ZbN^SBnM>%D+^$N3@mdQ z_q?rqgolskOll_T0g^UEBpjteP2>|HZO6zj9wz%hyizxXd%?52auy#?1YQ)#>q(Ry zA;0m~<8BgCS%3Q`&>)%++R_wJC9z8{{Bl8>bwGM;l&++1F0eoJb<+{a5!3!AqM#v(e^0K+9~HEB>`!@Fl2NS_@vsZBXA+8w%IdXSW0iG+ z4fprU$DZcP(yZ?|#wy~%FO~tbAz-|jnbCSiQwKnVjmVJ1(TByj^@S32OLY?I$GozZ znM`>qsnUt>lAvnp+6I_r|c5Tnf8IGQCdu87FfJb6W{+Fbb zL9)a6jb)sV;e|3tj`1r~x>cZF#bZeA^imZ^i!z^DJ!ej9_RIQ65am0xUIqmX2rnvD z*W%b5VAO4lWVatpgVX2NHtrwnN=wfLvAlmznK=6%V1qRG5^VaDUxQa5)j8ZMz05(S za;DPqA}|`uL+$a#?f9s>Zy@3#<=GI?bHYf~UBjut>O!X>;in3sj)qHU!=F!zvc*HB z+Duq{IPCq+4A71_@+pJ(?O|9{b19o#uc7o*r%JwsG@-W;3|}x4T}}lcs7TiVHFo`ICouty4=7)nMFcY}>!yv2<fHp|CuEnErtTYxt)+IjSaav;azB3htfEA#$i`GYIS3r zyVNvsNOpQD zzgpw`#0e#b`@w2U-7{2NTlJuaXvxstIB{eZVJpg@pTWt^GbB$LsL_ZNHs*eoiWU7n zc)$aEAdQC9b?{p8!`OrS)bij5Rv31-xRK*ooCm^pbX{pwPpXFz~dELvx*s}fL zj~^q^=>~E4*rXwb+GxX)c(_eFqMz`gZcgi8ei{v0yiBz#fp*)d1C=_9Ayjs3Ld;`A z3LT6cCriKtyyQ9!42xszH08kcSY^cP!A|CvXyHxEtH7icADQPxaz8iTaK5&4BoaOO zaXM=i5mq5KaEf=YW2YTLZ(ZFd>YqWon&!FpxCZ-j2hiL+Khv0sWTZd~_5edg8XZX` zzoaoYuSqN^NoQyl4F2i9VIQh_sPxB1L4hERXT9SJkoxNNi%y85l9PmqBbjc6H?87v zOFyo6-b}KO9?fI{y>&Q6OWjG7snKn3UPT4tLJ4e#Zmn$OH4Y))#T_(Oulf0uBF0w6 zH?LW9I?wKmN0QT9m(`x80ZQDKZaFoNoC3*&Lm45nk7$V%C9ciZB;d#Kp0tu(`?rtIHe#`(5Gl<3C9;$yag;z6BvcS zd&Q4@4ZyRrveeKF$@>wV0tk!sp11R)@CP?UI(e>8>V8GYAlW%{IN-@j5|=!b#0(e% z>_cUj#`z^XN>$tghjT&-X~f-22@gXgRI5NU&I9_Q-sN+t|<({=ZC}GXdYNpZHoRAAW;{dNI*wIuM`+b z6YC1U{@^}1D6eVGGO{+UhGv-oFX#!TL>WL4$K$DeHa8=N#`Xk1GfHY>!LQzw%KA|) z?Sf|357IUY{%%c3YxxY|qjK6Wb!+Ig5z}Egbdw^BPy3D%wC0lz`q7tXA-S!Qam#Kf zd(cur%cbjaPYqw9D{~n;$O3FIT3$!S_YzO6evX=gmFhSsdS}BC3^DSEbC6t}B;@7g zCx<~sHBB*Q$-8D*TH@077e3uQ;H4bl>XCu?zLGP982P)XCh@51*Sa1faPi=8nQ~4wEm%QF2)X0 z>=>eXj#&Qch!kkMrJtZ@e6v_yc6(?%J4j%Bp7+7HXHWUgdtxGbAA9o4 z-A-lYLMsmCU|&`LTxHp-K)~RZ34j(D`Q*3k9ANtmT!Qa>zp%S(iBJYC1Ev2@2;{Cd zq&UgJC>&Zal`*2BvDm^L6Z^2yuUzC?1V~#eSJbE)RC&XHPn&~F9>$*RJ^xovVvZ;J zfhy^qKpL-VH94OYN?#H~ei|QBJK6w7-D=Te5HzCi6qhv5Pd7{#;g9-wvt{xt#i&kw zy^%#uE84ptgyK`@wQPF%;!|#{ZcgYDB{}LpvXTEHS(3!as9BmGenBMK9IJo0%HY2# zt8`Codmos$Jy6s^Oxh;`X;p9@fgq3w$$bRY?k^`U)s2WnGr(Ksv1DLIF#+`OSgxT= zl0B={{Si(>y_6($KA*#5;_;pMLTI*`IDW_Q-FXTWP`6HNk)Vu-g|(FI@%eE-)MhCN zMU{}aMr4nx9YK4&cT^?YF6vF0Q4FF%2=6!iRTW8g5sKOgBgZ2S0-4^Bjh4zqoVf#&o$BbiQ0q*Q5ZsOd(%(9G}<59JJt_wK_y(EV2*GUEK(hkIml z#=2vH)?~HZi8V8i9)Bq%>Lk>g$n;-YuM~*(lTc{4;O6(CTZDY7kEqAkiSAP}{JjzX zU50oaJE6}0UJwC^e^Wq~%z;>hbx3PWraoYa;9qxe5uIkdnOPmeH|;KtJyc|Y8aObm z!f@&Cj6p?C|IfDrbzvQpQHo)$UWR>5C%bf@Af~DDidN6ab3};X&&U45CG$TRA^)cM z`v3pKh4+Eb!I1A_r=1ey!^8u|2I`OobHN6fVX7T^gI)t_hGu4S1FJtq;CaPMF#R?P zfYWXumj(P+ArdfjWMcha$vgo$1FA>^fLag7Q)0jbT_Y(e>E(P|pzou2KxGyMMS2?d8@wWJe7nn)mwNP|Opf~)w3QaOe3f5N2)+a}B7 zs&9U?aKzOzAm-F2&x;s|mUmuxF+ydfAQX0f-{ZV|9(epr~P4gG8xZWPJ0w! zaEm=~hJ+fRU1~?Bk3#d!`a4BT!e&tl`T@j150AWA7p!aoEqS^Cb#jgJJyn#I0^_VJ z?H>fng7cGI@hj9}^BxL^a3U+)w+u1==~GEP0iAsKl%9-?Y}$^usiZ`yPvqM3Sv{aZ zG{#yauW<>l<-AclU1S$toG!}#!T@UmWXDhYYc_@%)@PJuG&U?guf8SSg(uUj^Jf=< zPMN1zX_ETlY%wX@>)krkR>)S*4(n_V)csp$5B9`_9>7AG7D46%Jdh}LGP3Dae$=4{ z9JbPc#W(|Uj5YtG_322ypQOjPy-S;~+}!|=0P*l^2b_^n@~{a}n+J_V-=`VS55bnj=& ze*WW(_q^x*aLy;j8mu+%dC&aC^}8-skCJDzL&C*_&7$@MsNH%1*&~E8%aI;<6?NwB zCJoBP@^mc$1;fzT(7j(8?lHK}@xvxoAm_f9!Vbql$X-4$LuCZ>VwVIyCcR$KF zF9~CbR4B$Gp+2o*1;+68kT3~4TfcNECfwitJPwLHC{(vlG zDa}pviY3Aekcl<59C-pPjiJxvImNzS-E)g9@>f7$rxRP26uS#A{9AnK-Q~tN zTU~!HypNtArI z@@N~`Ss3R2J^e9thy}1a)%{2iOz*M>>Cy9xu6GzA136y^hhL@OaL$N9Em0=$Ah&(_ zDfqRn@=Eq3PrvbdpC4-41-G-!x;<>9-h1$%2tI}^vTVd!t@HOt*tN!o{)or>9x-HT zT@Rpq+@)e@`bHF&ao=>XJkyPm=->8}nQDEk5z#hoM{RQwT^mvXf+y#A>IBG9idRY* zw&}24m;uKH1F96GzMB(eMU`sSGxlVDY<`y>R(DaWw+lnB`O9p)FP%_vka&$oC!eA| z2u9BT-qI#F>TV%eK%b3MMK)Vj$RL~Dup~WvYKi6vSoaSO4#|Y~{jgdDNzcRItN(hhfP3rw3i*N{pZYzB>KS&R zNifC&9lQG6)9{wD@sH7h+z?q#)3z{fD?kF)?fl`(D~t$-rPrbqF!4_f8I>SZ*R+b1 zI`S-joOwrOuwD5TXO=kPY76(*8#uplF@7xEx~}SiM*zI!p@5Z3-H)wFFw*O$bI-%< zSVLD1pAU5Y{7+xRCV~c-@4lir|cnyj$ zG}xC+4-iw8DfasuZy& zA%fJ$tz!KIUdcbc@`+UI&flf+L-{JC;mcG5&N5h)B^y1V;3QNNNtkJ_mk9c9l{sQ} z6E<*@#Xg4;(g{0;Z1KxRvUE#N#Aj`T+pJT4#H)#5F}Tw;|nri6<~h@cGTxk ziIE{_H7)MT`(jZa;bDRVyxR_~JtBS_T6KLdzt#w==AUU27$(9HbeYPw3Ry6|hs$1-dAhI~* zRAuherBrGU-)>;3>_=%W`CKPaA!K;NSVGUc1a^Vxkl+bn$~huwfgnD`joPXmEWJir zHVc3a28dsy^uujs_y6a}ADsn2cPw~DJ-pA2;Jg^6_()QAq*eZnh7~5hgxn;$IW#*u zdfFqz|9P2%stz+owT=GS3f@s+zRa<*xsQTpM&8sZ`*#Lvsrha~6 z=mBZ5zFa2**nE0Mo7v1^xQ5Q?I|?nx+B#<`r0U6#;Y+HHN(ai<5ZKcP9%H&z!BLX) zv{nSP(t;7|lv;G_cStGtu_|A_!bgA7@lmgYx*xA)p_}QWa}JJ2plk~LH0xBT8i``; z=|;+22s+Yn2gy_T^|?T7OtDhD@Xsc%x;}=Bbi@_)4bmpVHdh^;n7zY_Y8Q<18eXCH z+wTKg15`6psW4lwBAo*Vu<}~x8BUJxdY2>l3uyxiSs<+htqJm&)A7Q54DN_8b@MtA zU6d?eaFe4dmtKiVex&}&|MXD6*PHUDVGting3(}Cq&1$b(Q2x?5dvF?GsRg9Mkde? z<08rgcVet2NA(|ud;qjTx%Bu#@Lm-wI?g7805J8?X+aV=#rfqYpw-!bNg8~`0HFj> zi~<-#R8h*?pZIjDf!E@5Cn-Cs92)9UmXaxz+;=ufAP8oZ*;xjlrOFgZ4m@9 z0I~ooD8R6r3EOkxOKS8A${&^sT@dolWty1u+Xmt#RI?k zR|{d($a>l|)$TZebD;e<@yQ5z(FS*Vpc*Av{235 zrm~R-^dxzd>)0m-*PVNLFHo34(T_!xiKEO5v+^7K1A3SszM#<0&LiZnBiBk2-Mkz zG;Z#Kv>aw`d)mwvNm2=10f-|}CXF>pV@iY4r>DE~&ku)Wkw9(rI;heb0BLlB7Z)`^MGcnFzvUtB zaW*qDou@Fsyqtn)QUc(c99)Q=_z8V8Ya9ug%h8jy;Y44$HAADv*yi9R3lD)X*C%L^ z(Y9~IA&@}jOTaL-fcgWoK&>+AE^8wsRn*(#T@=(u1gTx5?t`!Q{dUEwgSRS}b<5jc za`{f7-^>E!LlO7J_{StrW;Ad|D4hhJxZi5IH^spm#b(o)Ql{cV-7+!XTbF);m0Fi% zXNqAeZyoPYG@=EWvPy@QOkbZTM|!yeixFHuBl&(jB)iyc#Vl9~U%HDWl(!N&#MY*i z-Jc^9$*3ca19-{+8AI-DDX+xHclsmQQTeSecB5Yh^Lfj-fO6JLTrqiqH4T|xFXz{q zF5>BG?BDA3Wv_9O1%Gx;Dgp|z?;l#=k`9iVGj{j13)PSjDTe3)-AAX*^{&?ZLFN?r z7%^Dx7rS%uS*6AveH}CmSS%0W{JCs~b4T5E*_A@MwIK6`c7jWx4XQ|^2$|TqSKzeC zU7ru9w&zP%(PYLQa|#%2E^CAkbC?Ff-Pop`E}&3;wnqYcC-EJ&c$)Ut&m#q2$${R8 zs}cYesS$KvYm3mL0^w>Co|lG@Z`?q@IRvKJI~E!-q_a-VZ6*-Aa6`#=aZd8-Oppca zZ!x&V5D$Q$)d+y~#eNUx2*=@PAJ*x=SOeSjDipvkgj5qjtaTDhEq^w~@r$|Wcaaog zKfb}`h(OcCPvBPpQeEyP#VGblkaL%I&to9FG)L-1>38Y6JKzX>kmt}ix;>U+SVBu7 z`V`7Edq9w2)?+N!PlUAll5ZLz`}4?vzbgF~xlRxov5;JK=x$j=ofP?H61IeGW(gL| z;d^46hiMo+_kH5GhkbR{9>i=R%QXW`iFV|XniYmbfDM_Q)MYH20f@}1&Bb0SVo)3n zRTeI$3plB3=zSreJ6_{e+*?D&9J4B4@zW`QdLxm`P?ei)LLNWE?elV*iQ4rocV z07ecyNtqzPsu$qE9`k2H%Pt?Y1z;0IY(ps^B9FzpCODZ1=gU^GXcttiB7<9uyvEif z>;hj%d@@-0Tq)uW6K{h~3l;(m2&dfJ%%O9L{}uHxtWp&nJ1_PE6;8$cF!9l+YM&G$ zmDA8a-kCHag*q@GKY@fM8)2V}NCGr{FDQJe9<~5H-adg`@6wQq7zGJjJSH-I2Yqvs#G;d-w`e?`AfWN1M7-qj+YNe@9o0fvWM;FK5Q62`2M`!vk^Atf?{zf&!?<-?m)Xdr1(`mA8apfy|+Otb=+K#~lezqAcCK)@Q49p=R9%-Q;>k}|*NhE6LhG{?Y!C(G|v z8P3WTqXgkVPQ_Bkc=rgx@?6sD?WTw=5D+hZ1Ti%9hwXZq)jZ4=8cS((S;-9oyfNJE z;NnydyDP4RcA7(?2Kz9&RjA=Dxxh*EODTv&>JvdwdSU2IN$HGBkA_EfoDNX&U(Bw3 z$giiwUNteg5~lj{!nAeH-Jx2_B1RZ|N0;C80Vsd;YqP{x<&&4d7B1v}(aU{6pw@dY zV7OhS{0r>Kw3G?KVfG^Dq^%Fg7~KU%Gn#}!9cU;KD~Mp*MaG%V}+ zONzgv#P4Jv_PT5*k3EYv{jyd_9w%VF?e@K(TJtb%2j#+)Jg1xCS@>aZOKf@=eEj}l z6{~blG4LD$ykH~JlfGl>XY6J}P%ab5P(2aGPuV zpdkK_hGTtN^$-S=^P4%XjY~$C+l+0V!xqXRA{{^Aco@Q4llYCZRwawv@gMrH=N3B> z!`z>g;1dE95sTKa)uWUD28-f~yAMJ^o+QKceK~r$d79jOa;^dFxq!2WthtFxbSj|2 zH`^S3Bh z+pkx*ZchbV${LZN7#3&5VjPD~e&s!W_f!}_L;TmX6d>N4GCn>Cdt5Hr#popGHeA1g zqgZr*boLtWK*v^5!~vENjW zP}}{_2SE1+NXaonnH>M=-QOx*|F=K=?QZ-z$bXeP{V||_X>f#oKxq2=vbpJ4xcmR`RGT*xwG zto-6X8m>mlSF2U>bi6!C+;-@g5{rxIseY#n7IF#GM7=+8^YDBHxX)Lx?v!lM5q(qu z9e7=Y!4M>i$>IV2=7VKwtFCpo@h-~@*y;VJla-s#-B*4L@KaTq|G3)UK}jM0OFT)v zP@+_aN`o0!0y=*g&;@0VD}M7din)G+YrkVF!{V%5iOAGXG{)t?EwR1ESMP`3Zh&18 z1pDs}sgA^E0u{jN?e6IqBw&F-2jIOZbp3MP$@=EhNcHslv8VeX6lvZ|e1A=3aB9?4n}y?wEOgZOQ^2|f$ah`?>Zd=>iF7Ydy(TaBIzbD# z;`jFFXZHIuGGqBxe#~y?i2nwyE2Ho*>GX@77=W~m)9~<5GhVS@0}~A$m{@>w)pPgm z7+9KFD)ArGx(q_0#Mb4zxd8p}-@m5YBDt&dDfl8-Zow6(m`Ga+%~mQ)#bP713 zBPUPo=Y#!}bWk6&=SN0p_l@|e&h?9w@1L6qBq+LHFUo5Cik9*-j1s~dy34>w=REq@ ztah{Y)c3Wa>*NDKz~$hEWVh6vun(>AAcO0i{qa}N(FdGgk6V#4?_AM|!Bj3KNLlP{ z87_UsG_|XbPWsv=rSkeb?eXiS8wZw?+3@8qD*LzA12z<0osV6{L|!2Lit~iMmMjq= z5A@xt8qfXkplo)NMto+1j)KLX;C9v)G`qZ z!sf;B7|K9&sU}Odv@bn1-x^xKMd5Rr9YH3{x z=~XcaDlp7@lR4D&_0!&w^iPX@Jel>K^}i`Yzj(I&L~HeQ%d}Zw&cHbNUj6#J_vWA` z#DQW8>Ef!vhFkA~a9a+FT!-r8#Q|KkG86`wcHaW)q0nY&phCD=95Xnt-X-xlTm>+D zE{eI|$>KS<*2f;)__fYq~mCo`BMGWm8Mk<=S9E*Tg-9!DWH{U-YJAe+!06{AwzgG?P5HYDKkW4r&e*){SmBTW!Gm+D|z@l%`dtx$)#gHe~)-mnj z1O?p((E}Fn2nz}b$m^W;Q`~$WK|_oY$TkAO$XRyNRD)Q#A1t6g_O=0so-5d-^;=L2 zj)dy%koA^r3cPZ@_m(~MrmKWZF_RbxNaN2O+qgSjA2%uq#>bAV&N#U5MyiysOxjcM)Jt62M>KAW(uiXUzQ^HK6 zI-nWSSu0FtxfoVpQ%cwabcgxXBi5D273|qK)g^Jg;M@n%1m35QS>BVMeQI!=*0z|m zeb0qVX$Zgfe!t&#$8%c2>Z6i?VPnG#*4fB|<{^r|huySTq~OalQfXM`*E)V!s%j8_ zzgWVE<)FNyJ_2!*g)hst2(R((B|op29&PDo8WH%#M@V?q(iL5jhM?gKP8v=$*cB*Q zAdWD!uAcOiqD(AhA#kb(T!?ACu+|lJlb3HBJO`}Ihdx_%$>5;ImwzkKFjD9~B#ubn zqU*V_=hJND+hou+3-y`$>#mW@*197x2qsO@#=GL5@IO4h>*JJlq6Fi^fJlE_j%pORrQWG@z5LrzBDLtdh^*KcMoTwB82cy+S(@d~H(CRfI% z+CG)e_8N!P$N29fx$5`@ny4cE7tWUmOq#n(0>InQn0}rCD+Q?_HnqUw3f%IsQ=@I& zOHH~l$KQnJ-ISsMwGUQnjyw1RYD4aKk~&39I~05Y#*PJv^dJ;ZOc%uX=<*A6APnFK z6KiDa2r?ETcST>odA+JviFe8K+TU0_dMSt1pQ3UCk+JA1lwL&F>L77dnk*f>BZHn-HCQIf%jk^8pXq04>4+GFlRs;mhOlb@CY z+D*Z#tMiSIm`XdSWNr7>T#+-Nlx+4V0|*vIfV{8B&+76sU}rENE7mRY0=5W?5p?rD zzbmf4+%3cu_^IapS&*6sJJ;AAX%u6ruWI)ldQ6ef^1AEoX-D5&8!ZTsf zC^oA6(-u=lXC=zZolJs-khjAdH$Y5-Sn5+p^xGUIo#}^}VW?8mA;7>v;Q*ExEgwwf z11*vANajzrxn89f8`OLKKhh`_wofCNz4M$$j1=2(5s;S(kA-FwFH7#;CX)Ig0M_=wU=56UmlR2-2TD|h z=;sdz((BiLeFV9(o!al zri@Z$0Iwj5@8;8jDDhLal15^prs>Xe?wMXbB5D6Ow=IS{gbgtN7aO<#4mxzAb z)2~g7zoSwmj(}YYmYN%MsR0d3fSAM6t5t(QeS~djuT>%K0MW+;HO+qe9l%(QP=!?8 zvU>jyeMN?@NF@6agoo@Km$|Q?Nc0mBv7sAH+&Ur0@d;X-$g6E zkR%)&Wb{kj$g7#rMg&84+q7mq{_vXiMCQb@Xa}!+cKH3{RKs)HqG?bT_7bXHbIaz} zS-lU{OsMzlrxw(gjnIqQ&_A|;QtaQd=wAhnt_~2|2*34w<_kX)ZEc6Aw8f4B!p?ts z8o1&QM&!;hZSlnFqOa)z)U*R_!)Qzmz$B`xa@RGKoyb@$Bv{Wb{0j|ey;m|Q!*W=VKW?|qI~Bxj-QGvA>@!uyeeD4P0!E9>7w+x;3y zA(p^eOBVq_c-BA}5Pt>&3RD-1|8kb;{r1f_y88gH_zR$@UL2Y5Y2F_8{g;G1Cy-yd z?eOjA*N<{nwy(v}qaz|KWuv+Wf@1rI*-;MRov$_M9 zI@;u=b&!yE5L&=gR+lgFJKz-{@VSFqkp*d zngJ~O)rz2yMC@#|K$Ir)YSU_FR_t;fb03&xley3n>%I8zW9=?f?R)smF{s7t2DDRD znHS$zeu9efas#&+mk&|^jwXc+g|U_UTzPoXL(dt;u0Nhs8JaC63Jl!x8Q19tuSmT* zTsLI}b3YzNDS`t@4jPg8+l61^_5H+@WWx&mf2SkAs7*+0d6`*fH zGFN6Sl#{85p%mm3WL#D4I(;%=TLCIC4gm`u1t?4_Fd(SLF|Vzt5jvr?b!#wf9MRG%X($RJ55%@AN6ZJaBtAqWCgSt8 zTOjxuy~z}d$^>XsTd7ScLp7`Pq$&VP%1^Q|9YSmoK}j>GXU7= zpVkXtMfcslJBFfBhd^LO*$s#{xE{6kCCKC~S^XBkgEE&XciU(|isob8_}Pn* z;O7HvSosw$ti7OyUl^2Lk(7&+BAuGl!ptlO3RSa#?NY(C5^(s=PY@k&cp6bqhUJH9 z^{xeOIuwI!{l)oOfjyq&78H$peturLNQD?fH$^QUzsgI-An~~r^JejT4C34VXuMa9 zs7r;lcbF6qD2EG;oA*Ewy}}Z@zY{>F}4`hdeS8bKKrA@Gx*zFYO6SRHckr(-}z% zMg29QoUe)<(G)vT!C%z(LfLr95485MXyv_N;3fPHRHa0*NFVY6h+>3=iDz{=VCQ-P zvd3=f?ox*)Lhwid362%b4~eVizZOHi-Sq%yP(c-A6C6J-nH<9f61`n;HdR-gO8x+M z^)u4Gwgd3(J)D6Iv2t1UJM*bT@Kd47csYxmHv_y~KQ<+C3FOvp?V`-jL%lU78&1zZ~WV)|0V`9NpE^VlM0WC7$)h2^D`c~7SjJsMvKDiwTYe`JY1 zj=$z8%FkAkhNsZDL=CO!U&;Th+F0_QI4Cs=OJestAQqT`mbaD|s8e^|B8(8#898zR z+!g(NIP1x%c;X(U>3r$6PfcJcg<&yg0I^+sIX~XgcmB-NkL2HX^4&@0$rPLhtsu&Lv}z%J zxkMFvTgmrlt>{GqjL36sJUaXY2WC$JcKb5^MSyr3U^t(CqZGf|l}IrWF1RXC1ljKi zs6CL?Q*~vvDAI+2&wh7w1eb5(gH_p~ku7q?&;0!-cd4Gy-aIO0m~qop3HW{}R%_Vn zR0??X*u`)oJcru;gOvdld(i1RXvFqE$2f_g*=X8a354+*$^883?nH&4ha!OqG5p)r z2(EtQjJJEi{-Uo+_IMbXjjx41vA=~-nH$YwSyWi|$(QH`njH-}w-r#np-ZDz{e^|B0VkhHevAHJM(5Y{fPM;{Ei*)puZ6wKBX_l z2|5aZx|sxj9B^^cBNNy+95!B#SXd<=WxsH187y}q1e+LXs9^-oNmG_0`n6tCS}^k% zXU0Gy`}DVddyvUE>}Toy5pOrpS?Hb3W(1=C1uE@A#Rpqy@a4KcOoLnQU?Kb^Frl1m zdklQ9j<(yAMJGbMADTrofnuNAz7{T56QJD8b=&!|5Agk;7%)lFU)fKDazC_R{~T1Se45kXW*&P3Zdes!X!C)U{D_@Xm-3PSHvjSN$T&hp2WlWcfB`lm z-)gNBacF$zl}0{YiutSDMCYekKQ`6B9UI0NAki>h4*2cs=?oK@br*;a0DWIPlx(FV zf{2;|@RjC5T0}v`JZYd=EHtyyHBC+cRf4?bLu*2?=wHOr{f;O0a!R*h_ zW8iy9H30C{?KbL1p)B6F9U>G+4mTE~wQz$G@<#fpipvtCpEF5RJ4s(G!M^kl`c`t1 z{~FnNI>M@E1&l|Rl%IaF+f#@!7Sk4J|(@%E~ir4hH*CvwV2i=q~T=s zo3>E`C{N)k#e|O0rs&V$Sa7#;swOJo{MWR^BPA!M3c9?ry{&q0fS^seZ?oc!)Uper%0r1tr6Sb_D$FLdVErVBB;#dYa%ET*rZK$G{)!`zu2OeHBk9~Yh za*P?fFE2Mo6GYr{J2}zmTg8+QFooEXMfgfSmPqM{v<|~GdE1HDi|VoAF`BHrOJo7W zY)Pfnr2(BjA(Q2TapL(y^3V(gaCq@c8M*jD{gq+hntb091T2d9l4-ju6le&6A+$L* z=uQ~Dl*jB)ky$wh*_eNDN}s}mmpgOCs(yj8KlF@5I@na&qf2nAbzy!cHMQl3$K_smod|LY_%xa zJl~A9Otp-C^ND)$^c5vB3*YbID=&REFNu`@c90!va6Qp4l7J5W2Csh8Z%4soY|^}( zzSHc1CaZhMOT=W9cl0p0mW>+dIH>F_11OYC*D*;b0ql->%b8Be8Ye7vO(JYf2DZHR ztd<`oMymqPZ0s*q%+$t5Yrych9Er&q29|;twqEV;8$(xh9$Ir#P%4?H8I)kNq5W&& z3iJw6Bnxt8IH=0vEajGPq4bRCX+9oPlNIpYJSotq^m3USyQ42lOcQ^(n6n>nb~G-i z&#_cc)j#zn2`{$M#?YQTrT1}Q;c>_rHD5}k}>>hz@5^&Y%?7#lk?!O ziyA?Q<=c;mb3AA$Kpms!=L?g@)|Q*b98?@i_Km?7=3eX;px5CN=9=2=HfIu5rV4|b zn3R8Ta`txUjrK&{)M7>Z+6J0BAlROBElmA!APjP$ z|6xl!X9&c(swUu7Wq;4U1u>DIE{u7GcCv0LzNMPG1&+9h`^v%>U?rR6V3roTqT(Q$ ztUTEua(%28;yfv%XXm4K6g-KYReD^3@2Z@4SG;aC^q)6@TkME*;;Sw_^4-1u09cB9 zptXGD`ke}j6(xIcF^IrX1(d%gJ9ttl^dv5&7q6i4E{0HD6XQNNojQL0WEX1_d>U3~ zPf=LpOnb7D(xEL6J62eFMs((pt`z$M_JXGaOw3C07VnZ$?dkOCW>K(7O^({D?OK>O z`SrDLoLE=nbA~wo8do?SRm^C_(9cb%(eI_&vi1g5f&N%RPLv>HOn(6P!*H`_%=^fm z4dfPY?M9oonSO=(4f6Yk1Zk7S1hX=?9>S)l?qqSkgq!u6haK_w@>1pwr9Oqrrku-^Bk(S46>*e;Kv(Q%?1b=6=hw%$%(R#zOo;?zgF znX-7N-3Y8KS9Dd;H_&L?dg>wT}bJ9SFm^|Cv7~kgKPeehZHY;!!Tnf1QG$TmT5%%r<&2 zL2rC${^+U&)e|Wh;V*Lm8y^m!E9_87CZVZ~`+u21a4GPrMZjN*GKudb5EP`bDtH97 z#_s%eT0;<2eFjmuNZ8(%aYM5V(tkZ{4rDf6(Rn_aj+kF~`3wS3^q6C;YFw90@eS2IcfI2%O*Aw90VNqKw3B>jA*t zjo~IhJ6BX71rXRKM!eX;oT~W4IbglyGeFnNtq5s1jBrL3NZA60hVby7FM{O2^wOZr z2#9?H;!#ksyYGj1V;3-|6yTN_F@4!Z^1Dl|Z?Jx?z1wXCHeD{U?E_-$E)bES19>NN z8Ep9G;W{TPf>tL0NEZSfvbf9j+e`@Nv`^*#?`X8&GXYSdXK}Rtt=zJYSm5e#WJVJ| zNQsp4e$4oXJq2PV_? z<*DH`8Ay?7V(-hvU8 zaE_AkKSPPdDQ^E2;_^j8Gfr-;$@jXc@9~boH(~by>a?97&YlN1AAXG5Rcx3zkQ@g) z55Prp3+;HOGr6sX4iGssZDilrzVSZnSdLfNCS?#q|76{&lD3uj@#%+p9+9yzB)^*+ zHhnT@d#5icHP6&+VpoTZF3Tqm;z_>qD1UySFVHr0PH|NeUQ~kFSUtJ;lO#3sYJjco zh2#8D?aBVP*722`=quCOol%8tJ6B;3GpCin8@}fenwk7HK`}5-6c7vHFy6)~%G>!t zu6IaCjI>0nDdgbqz%EI6e zljoE>L+py9zia=n_JL)^1F97Nz3|ySr^52L)xeB*x=R8Nl za-K_^>>1r+o`s~?1PHv@s5wp%Z9~U0#SKBL&`|$;tMzi#Q42rH2JC5;+!#CB<>mUD z&V*Jp1-rz&O5@)1j}Gv}}I6K_pBGwu>$)`uhB>e30Lc)OZZNiG(egnB|j*`i6pu82@yA?(iG)5#Vm zE>z1@$ra^qL|o!n!t z1ah~ovp3WZH@l}7NS*rnd_VZDLo<5#B!h1o+X4AE@*u6THhf-H?!WdF*lgHpM}cYZz>LE-7m24KI((Nzx6XMgLOnx zo@PTX1)IK7tUByFAHAJcEotTmn4I3-w(ejwlIxwc%$VosSZJu%)!wd;XLolUX|1vn z`;y7H#2brVlBL=DWu>1CUDalqI?r0$?JXCyDFZ|@l)R-y-le|(5wToT$lE8<0f;drjO`V*O}K6F=a30 zjJDIk6Rb$K6S%({RP{N< z$-rr6o=wl~iLGJBahQvaHR{H}9nVQ!M3fcN+b}VeIpJ6&sDaqU$7_gaO>E_-<7DF( z5}sYKI<2|b3yEY7Ies36rb>3J)8(r1@^6Zhi&7BbG)+02$8{pi@Vx^U2F6O&T{_TW z`j}ISUL`DU9dCV{EQXO3vhM>onVDZm%_msz6P_p-Vmq-P_5<#yPfV zSV9tE-btnFOLjr3Xy8U+kksVxU#Z{WzfVZ~7)=BnvCCoTs3#qrKr*a}Wdz0IeGHU@C;WYtU4)G$gk7Mv)D0 zJ>XbeplEe*{npO8Xz`3hk@d`@iQs4N9Yfz618X>+Or#3g3b0v4xzv0v$h{*PJu*%F zERV^=o;QxNaB!h|<4dVLM?_v|ALJBw33F2T=l1z3IaG^CUq$FKjFQ47$64l|oNH<3 z#~Q^Pl89gPuU$SL$$!LL8Y5p^lfsne^YP(3{npvh6xcvJE}ccfEOpzI++do2Z7|m~ zno7h6ak+Iv#$+SwEHyCGDuGG&aaI!lxa@-O`cLF$Oe-Df;jyyC}%5oherz@ratkI!q( z)t6h$Tv%ETU;`!@Te|W;FfCNB>T{+$#HQh9H!$dtkzF|$7q0Y zs3iHXFL|T4<)W)@n_7cswKLvYS0isl!?Wq^{j!F`v%zoi<8F^r_&IY31`%cPO7o{T zXPn|RW1JFtaQEegC2?={*d}&HCN|K2^i{bouuz8Y3{*DDgi zTiav3hh01j+ z983-mN)wE%6JP9owwyUKFdstk?%Y@!>hx)@?-;Yte)=SlRe#8?VUPF6qrBS_AYZ4V zVg(1k2oHxV*xxJO3zTiqVi?8^dJ|NiYR|0pC|{7Kam!(e5!j1zzZ9gS+5jdiOR*0| z#Pq8tXjqRo>NnjQHlDtWr-$Rdy_#NY_K%Ego#|<@g1dfVH)6n!M-DDt$TOhYB?YZv zuXQtlIEgfPJ>tkFZt=TzU@kGm^rMv$mkJib@cJF!WH%K}Z}b=<6d&VK$Md=*zL9uO ztqkAbdaY}hQjWRmKq*mYWls10JeU^S1)d)5=-yG7wjo0ed|BFCYZ~;TRb>*A7xj{6 zV)9AcOQJhfwjqq>oRZyj30ivDf@b4Z(8sT$I{4Z*l!;WYj{oS<=8?^C^{-bRaSbp2 z2=mJ3+nKGtjM*@;LHIY;4%0gv<}vE2m~cO9#`zv3nc_2jk9_&B!z#E+xa>%HkulL& zsd`pa$)zv-=Y7j$%o`lN-iTz~wRbq(JiG$5MdlR626kRbWuOyuT5qP?FQ}3x8hDKO z#nP}z3i@hh_|(T{s^8lBmtPlbOTZE{aK;m*>ku8ft$0VfY?V!?_|&E7bEe6xqxa#r zTQVYEWypOIX2KCnD(Zpk)zyB8QD13uF)E*)Z#^KBv@|!SMG9FcO<3Vpa78_7MjKn6 z-ge2i{OKVxcKkCrffWw-6Jj<;-6rw6l`-vCv=j>4K%xq@iq;VH@t(wPb(emUC-W>*RPoRLC#$YK(DTFlc zGB!R|wO(Dm+G@8EVUKx)V73|;K8|5`5bbiuV|KBn)19`9Puj6msIIIO5XuiK*4DH)A_rf5go2}fPjY&b6jW8Yydfo7Cz87c7KN;o~ zQA!NaUNCPNm0yVcZZB@zl9l54>J(LDo+V-!0avGDfA2&AfmkP+s+};nB`5c+o*UHJ zA};mf2LFs^iBRf==#mM0@}l3q(a;0V%AUF(sWCT)jE+OvBl>Ie0}`e~(~r~E%5^cd z`UN^3z3bPR1uv3MV;SI;(`ssllRA0FCb!?OXpiUNyLlhU)%P>k0lir0cc-!~cKu(Z=D zd+sB2dZ#WQGM!UrEHUCIr{6(Ejbs&fME3x*pV${%d3{{m$umJQtV> zwl$}f1kBi@KCMR>W`01FI@G4BSFX70VpsCchDJ%GqIemcohkwSJY zls`#KX>DoPr%3i|`w72hpO(Sw=;}{0^F!>^xp#;XE7L=R;_-?oX`)VYT54LS%tOb? z#7~_|qc|C+#f7+dD<$PYxew;TlffwLocL7;TRiVq$nOvp*hS%+CqCfX-R6o+2EJTG1aNi)DRmi@mVki#;^yBGD1m zo$+RVZ(XMN+8a&=V;J_^x%e9f90HdJDd{U~1@3cUWmgGNetq)So2v!-(`2){5DC#MQI)50=t!Hv`?9Rk)26Yss>!#MyZp{g*gb zD~JqvAL{)njKt(;*qbSqmciQ6f#v;X5>21Fp@|7Zs4=W27Q8p3f z{oW6{33?VGG*){RnliUHmy!tJzRZi(Zj`q$5X$3Ooc z8&3a6EjnhI$w1k9au$xQ1*FVnQ8s36US*lhr1Y~%!@TvR`!;*GT|3iJFXF89(AWK1 z{54PI*Iy5b@!!K<}R~dNg7{lr z1^FG6{lk5trSbI+Z|VqoEo%OfPb=i4V60N7yoU3dm;_%j%vaP?&$C!G5It68OQb-V z{@D4&tlz0w$C1S1VMRO)aZP62eHQXtL!AAZ_P#tE$~Nqq)JVqe!DJaq(i1Vp8fGl*QCglTvJ55~j3pv_*0P4|SrTow zA^R?7$ofQv84M|8mu(o!d#Rq~{pB{j_-IKhritJ`?{~|ysrB^f9LP~oxP|! zjr-WUx1b9#oWxm&gpB3A}l;t+pp7$ zTyEVaptR+ogr#O^C@px*cQ&5Z=|Ak3F zYs&8IIQUY=3M9`f%o0Q4?VBGg``nfF2d&C#1- zaoE@q%U}s>dPcv~pxk!VMZzMcc`8a2PCaq{<7W`O{{{L>Ws9)v4Qk)dd-ub=CfDY_0nM&~wfnGJp-G6D3GYl)GiByDR;z#E%W6;DH5LB1@B_EI4A5ZkssA24|Aj<^ z#1pqX%f+V`CJ9OkgPiIiX2B8RuituCr$_u2ry!VP1XI>BxP*2RnV)bbECp^@kQGbK zKxp`{oPx}mY-snp15%Il!*|0yo_|IvkY$%6scea~pSslp;(f*0e?1(-UC{CE*5O?k z;OW0I8Ug|CHIeO6;6|JOvsnp`MoY1r#flqC>u<@cli?rH{i=DE#<85n>-z-HMxj>O zhm~9_XQuQ!Vum>3+cV7)@RN6@VoeejP7Zsn-(5R+aph|fPFd+$dd??KR{E;nwG52h{BUbYdBo&AZ0XLThO&%R7AD@`Jb#NZA( zE84PzlI48r>?}hFPje@qnEyA7Dlu={Z?iB5i%h%5VIv(8PpgYFABl zFfGsN*`N=tMM=K|tz3G7V1JVwSSq)Y zkI&7WJ?pD7++UkEr_qM(40teKi`Lk_SY68*Gd<~pax>>Y!yw$fSOMZ2@=T3aj(32h z`rU5UN2v_?4q3r?Dbw8MTh<{&^BmN*NM@ZOAG6IB9gQ39>CqAjS-jIP zK_F)sUUG)*>z}l$Jrg>@={ojJCBK~eK#*m@nryz{sTBQa?dKKrVWTK8%vq{8T|xWx zzDiXt;oz)GRpu{p@$$K*NDCihdtq`j*8`=S9(8iju#BO#opUZL!5#U~?I`C#!OgJM zv{8;-?__3Us4VxOx7$lq~;DdqY~T?lY3OO15S}6mFCo^mOCO~ z!nAkcmVWK$Dq#Fw()66W-KD=BcFmAI>b3fL{7+c|&q>aS5X5ps7{KYJ``wmp1^067 zjZ0oHd~gk^m3h+X5=~?T-y}(-LIp9-L-!rrh+WY3dUuX+7CoLzci7Wr@n*Ln`-yv2 z2E=g3p+X6lBUltcL&2K|=Z*%R^a#fGN4oLFde_@^l~#!kn>?Pr9isTpVI--(Rbs@d* zX@HPY@q6D8tK#!M&qei91MFE`c+y}8TA`d}e*&*hhiKtGC~z%Mf#uo*YW$+cAdGDp z=3O|}!L*J&YR!F>fjN*QK<(UG_>B_f!|8B4GaZ=|+Lj$i=BKa1peenqV$ibIsYs?%V~ z1dA~_^e8+8f6pky9ww~rw(xDMrLR3&PUJlif^k7cGJl30jEC(L5%dh>go*ZqdmNOc zYNHLI9cL0SVd?s}A=0Jk5s&Z(+L;=@7^%^iEJP|a5!V`4zSuZc9joJ%wz`*Lu;s=G z!>2d%zZE3&mer=hmMf)LBw4PDf~;{;Am$Mwj$ii?>N;2tD_6>_wA4KjiXdgK4U2-# zU5orCpT&D&T;isg&V&e=Dd~}ACHQcWnX-w>B&LclR42_X%Z?ZQKFSEYWV$L+{0P8V z;`^acE2#j6%EzwynKm0V6+((BLz+F=&LJ~c&!F@39askJSDHK4pgp|b?o9g3vg6;}TW5s~Ty<@#{%A$zOS+$j*sfDLFzxKcdkE>EgOXL(+5m`M-r674#G@bN2 zUo4VoSQfGE`6AWW5ZN!LpcU`}Wi(^=0ihMJw>CTdk&>3zTbu|7g{FM+bs+u%EIf59 z2L-XQDVj{VWAJ9X|MNExxZ8(ZUHKxd*mrv(6>%FGmhS}Z+c#Qrcy}c^c2_b^b#x3b zGxc?^@b7o7wWWs2Hn=n@-P|+rqH9V4#yy--NK?7y=2PcGyZ6Eh>#x7pmp!*l;|mCy zCdhXfmb0=Je$GT?GE=6eJnXfL_N#PiJI?3K)F9>zr1k#zs-P8?tV%2vAi;k-F~_Qp z88tTVShDrWg{WL)-$|3$YrnN5fOp!uPGU z8s;-UbeA#UG<&wj=(P6odLN7X`HtG1FZNRIm_1c#069zDiP8sK&b6-*ndsHu?k{(3 z^>wKmMl2)AOxp3ed7aUtLWzXc2Lj57MA`4Yq?Y!eg8b&L+IFk(aJD2-2lSeS2+Nm( zXOBu~;n1j{h!CZSbQommA+AZPt*MUoo;|5*gAE9SwFT;3v$+u(wAc{dwJ^coSu?mL zxfc^TW_hsbZw3Yky)HSYa*gUMSSv^!LGs6nrK6Jj2zjTmCmls|WaR>}egawTl8YN@ zK_~v9Y3Rk~fXqEsHs^Ha&G?`fwqn#gcjZ#Ec)7|L=X{5V{si<4AD=PE&M9= z*g^UzJCV7_Idit>c|*Y2D4eFjr@c!o#cN1B_HDt8#mm(=4*@afyO$BbrN{xfnZHOr zH}@x>VF^^8eb+@e9L)L^i+lL%)^F0Kgam4zHGk)bPRi-lV+)V#;MdP;XKGHn8SFNu z+#e(w>?o4N?96rS>oY6A$`2PlO8(34fFFmqgX{zPfLV?7;Lxsps{V`N`U#I(5RlrL zO%r@%d#_22#nL(9yr@Ap;s%BYB}rIN5-CQ(8>u~)CAUN)DMn52`e%!x)gI0$^M&(% zFqh@=%dA8?3FpUE{$N|&%xQ!i6wJIYdCDs!>p+Z})& zn`HO>mc;zfqHx1tC+#TBV6*SqIW@>D96}M=-Th8eb#m-=!l0V!_}N)YE58^2X$xJ* z{33R-Ne$p%6OUm*agXGiDSWX>`A!sD9Z|bWn5|1CLy_7rTwR1qu6M7QN?NH2qgbwatdroH%DA$s?(bJk$Y-$^~e=^$M`HhC_(z*X$`Qm znX643qQB#kLH?{+ug#9%csFsrztQN#ZDo2zU^EXPVx&bdKZLmENAtu8r2hN95wVV$ zi9GV_W!_%d;W~E#<>1*3DJvP`l~;9m|eUZNnO2a0-~Pm)BfhtR_{4H>i%2!q3kL+VBGjUf-U91wHC+1t3-2= zC6-KkH{%o6QN3x2%%8V6!0q-*d@Ht1%Co!uq)zC81WcUacChRZ$hkNA|Lm-!7Z{ws zsy6Av##gTZ3r7ESM;IJEFseG-<(izdpC>uucQuy1PG&RxPEZiy>LyPEjI5POJJYR| z>3j*u+5F9kQxqoHe#`2|Or8f!gScDJXUAdUwwYo7>56P0(jAkhriQ(SV*njh9YbE! zm1dFr_Y0nid2yv7a6}nQ?1D>nZ_mgPTL}Oi{rwb4jxPmoA8S6Xd1vkw-|q4(Mp5ve zo2qWD}Vmi0rlU1u>bE6{=ea1>Ulcu=BayRzwrFybcp-y+LUdnj@anmj8l$> zHk5!}-beBY34D9VI`a=+>8@aRFCZKN1}V^}Y9;#oW!8)a#biJEDA+v-)aLyA9nt3U)RMRKV{Jg`z^X z8!RVyfTqsFGG?{a9`wG|L~j=P?tq-03M@=)N1M=?P;*e%+XGjj1-5gRYVNFe;T?c*+>LdU4WW#xxyrX{K#&H3u3!4;1NcGHnk@AWn_hiC5rk>|GOY)Qvk)0N-vw7Z*` z>1-VUTZ=UtkXY9~75J1e64qp|TlqnLvcGMC`bEyTN0+DR(u<>-uPZ=(opCW6UAPzu z#zX)duEX~kA!hva#HUYA8bGO#NZco>3woX7DC@ z5LU;F)3@wgfV2dJZwd5(`c(bph#YK{Yloyv<+^6UfbMAApn=xiL#X8~ef)!-bLqN} z-^tQO!fu03q;d4l655#Ihxe1ue?`l00NN$r$AOj@+?I8XMC`*)fP&&tY?>W(5&H-? zgWoAJW0Z`82C$RoWOi*e&$ujK4Oq$0NySt?azq#Gv1}Zu?Or;IIY<&$-g5VDK5qKw zAW-7V+u*p^t?cHzDCmpGWufYl9Qq40*Nf4NVvCv2H1t*v+P-$JX1?S@uvUuGMHe83 zAV!43=gZhFYuip#l^5+SaP5rb^KH0)Jb$zTp*6FPhsp!?X=lte6)@ zmf*;dz5ZM7qUJRj=y9|44~Dn4&OgEV$x8{EIBMV1%kAoeA9gjZ^Ht@-Fq6a`5CU|^ zP;#*S*=O@UYgNYc$js83aixOR+TqM;f9w5jHSJ8^I}F-=0x)7K7fHCR(RZ~tJtB4f zD0=<;5*RVwh_e7z&FWM|7+;#-o|{jmF=}YSeK<0=_W_uAyH=Kc*Mq*|WM4*^FK)&9 z#ILf0kdhVkbb%9yT z?1zNf^A8d(a+Yt6(n%uW(cY-3K?S<6Ca)=nRXD1*xU@vg+n%OK;7#?z#Zg zg7PhQmlcM;ij+u{Q_<=wLQ7uLeaw3)g{EPm)se_d0Uf(jfT%`^2bdQ0M}g&UpvO4D z^Kfu$Q_2y{o>^@*Npvce@~A1m#Ty8^GGw+@RLaFRE#rEm21Z|SH> zKKEW7AbqkPi=sEE?FuGZ+)QP+3`eZAW#Y>PqyvP!x;%%+@@rP^Q=M#>m;?gPpVGb# zCyRMsE@_UjvYROs#ntC(@Q#kbcidGjsn#`vwk1N^)sCVln+YJ>H`N(|d+gLCgkG!Y zaxHtc=--SN%-<3oUI-os0;ghW@7tOBmPPM%YoD02hi^xH5%C=QtQ!LMzi6m$_*a}6 zgEFe-XllZ6LY)I(o3Ea^mPM$C-e&*lD%AHUQQ=J|(t8So$=sGfrCD&gC)luV#4)bz z5w2v*NHyX!_UX++QK=Tavxd~Z0yEkhGAo&7de(6m904@Zs70@7W7?F1$JnL~>o8-% zCv36wB)4U#l90KuODY$cx7gfwi|ji~iAX%HRxLKG$vWQ=BKF|bTfvpFmB6BT?|ILZ z-7{R9C6LR${L-YMp-9)hUj;6b6)#aMuRDNJT{k6R4{7vtR~UYFUzoQ$v!yUEG$sxI z7+PH(EwsQPEfS`3ynQmzq1EEb2~)3Pz|c`13yL`1>w_VtXbv|mocU4)!^D`XRISL}^NPHpRVAO3N%)RnY4Sd6EWZ?@(x8OvyVqww z>qY5F8%IS@+^TB5e2{mxjhZ_r6a=hO@^A zVkNiSKD)nz6GdH(s~l(5T9cU@`#5CAC0o}P>n?P|iJ2J$YKr89(lRZN%lXsnW2Y zh1G%z%6}xtGTqdB0TT$&WK}?CvzKM6JqzGH0QF+uOV0hw_Reice*d**h4?r&z`E;E+M!x(d+EXroZ~?%DFiZ ziJ=Tg@%IgK9SUeO9lcTQKel>=SGb)gAb7A6OH8}Ly)EjzF@Mf!}}4}>6Bap(Zt(YpNV$+ z>MoJ|3;5<)h4>QZ@(~TCpxeeA90Ns~W={K2AJ=VB>t-EXKF>Q*i00LcwIO&4QE$KB z>jqqK1Z)0L>|6P^@QoRKHHsb}HEifaJ%F^d;lh^iu)bc?Luw_M`BbFHHp!?B%Was< zqkdI`EquD`tK3+4%-Tz#VOe^Pk=$B%YQN?C36^-r#q&b=ckceq&db`EdUg(^hH?rw zgLu=X0Uyacbr;z=dK9}kn}R0F2`k7$hC>%#v!`EO91m2vs7|uu;LiUIwf>oJyKaY3 zSGJ0D)9f#25}r@3)SX@Kf>%PkYA_R615PJc^4&6*ZIbUM$x#?pGA01&Q;~R`!HJxW z_v7xWTQRQtS~?cH3FfY@+?CtQ2YxXo-fGi(^&5@IvJ)JW^@MTD ztx`8aD>K&blIm#S6J@GfHKbgwzVi`Tc02tI3)@Q6_GFYv3U}DFe#|K$Lm3#qAyZ48)R^ zC1iTK;P3(F!vUzwL15)(N2Y_A8*_R3d`8W3w=O(Q$NkD0(D}mz zu%WID`{P0qwYR^T|N4F~6+J5yyn&tL7dE;WL?&m2ZDAGM{Q}?v}DQD#-maU%HT@p&KKB!+=v& zWvYHLQwjd6>vDVfS+S~p#HMinq~Ft;{VGx-m>X~L+3D`NT;qfY8(N z!|1NVbT>UXvgX@!u|eWU z=}1qG_j{}fi`ALqR_AINL-Um?2sDD)#o~zJuprj=X~5_yWW!MtdZ04MO#FH3GX*FM zsZ-y#6PYr-Q(Jbo2%3#0uqiC*WZD1&oI7*a4>E+tg@y9b%8|Z@uodptlC6;aW{*hg zSHm(ESLoM2HVCrH{JCI%H2r!N2C~1OBH9k2Q?GEI=S*LAYAb!qY3w%#k5ZD`-ZLuV z45lZM$u~ZN+WulA0q3{vjCC|Zt{MB*UWTlC>P@vy3?Qf zV3IxNs$tbtNK37osOQi|tLz`FwY-tkkku7i#v3JpePiy`ee%?xH7?~e)D6~rGT>WR5X1j?<68k~!KduFI@j5IIlz%b_nOA)5hD{W zuk7KSgImqr6F{_+V(7^D17RUw|9-v6KVh=vGybFgu z$r^1G{jP2yNba4({RZeLs%WbPuBA{Um~STF``&6`zMQEy;PSJ~%T3!{+>OwNjQSBr z9Z?b!b6i7(<0YKjR3m#b9}>&4uyUn3(^UTk5`-$ zWMyX=IvoWsXMaXJ`czUEvVSD?6&V91!wcRHCh;)*uLzk=$7)9RcRsmm_<@PJdRX3a zG47>?wYyI>r~KGN!sSo?N^+aJkB#=*<4%DTzero~N`sF~H0evr;V2Q9VO(>iW>u#= z?`CbYYsjvFYdKIcBkwt|BqvjmF!BkXYpuhUAYZ(_Zhp=Q)ik>M8dbRQg3$&9J>qZ} zOWgZLH!rG;l^;_$-!R(Z;li+Hx)h+rfLdt)VTC6D4L3cP`fGp$f*&WJMyRkGaZ_xIz;^_6UV`~apI=w+Z!}f6suE5@O z<&B{dZF{xsZ0Y5<%nwHVS(7cJG)q0^J!w>m3I{~`Q>|@w6f;n)sY(W_F6B2%?*4%@ zGaFtlEmH#G{JO;eW$QzMSYJXw<)_b`yvsFCR|K-pX2sx0QQSp?a5P1sjO&b?rzp_$ znsCfq60sCOs0KiaF~i%+FVJZ47i57sbF|d^;|pjow0maK60;$avS+LuNOgZ;Vgu!3 zcR>X@e|&jic`qv)Yby!by$k!Kdd^jJ^rUa!EAs{T0}i$EXOuFR=5lw-dZGCW2;ZHH zfjU5ibCl;GIJD?M0I>Ft`=Q`}UaEn<1u5AV6F8~g9|~62KjWJ?1Cu=IAXm5|9={)7 zJ{mj@wEtY~n|s!g;9KX_ z!t}K_!l~ZfXmE6+b}<5PH&?yXISvF+9flVp?<-z@@G*8U599EX8{_lSF0Kyx?re3O z7QFpMj|9SP6W;Y*&?@(KRbS2G;Xlm@-$2mWGXpeBJ3xbuG51k+vq|1&(C%APLomE~ z7ksm;S?TLY0p9yKcoGh^l+>lz<)(>#g@#Uj@yFp#%gf3Z_X6+K!J3N_CzU zwcw8>-@flke!oE)3nzd8C+$xWrzV~b`1UBgvUE(N9QA5GT9*@+oFC1sBWy6|$HFND zJ1g{RS3xzuz0!4hu&k(XRu=COHr-`dxAAE_Qhm;mVPR?^4TPMO!_x)p=*|NtPzVihgK28ifodEkI=U2R= zKXW7DNWEe0_op2I>Chb9-kSHV^Vp3JvG)rvu}~dlI-LNj6VYBIJ~|INGwO`Z%-7qvW;vIN$Qw>k8+IP$E$pATqsoUWjTZ z0{V;$Cu<+z!O`rdn)Cu`T+?v9IN!)Hx!V4uuFJj~J$XgDZ+^DZe*;Bt;HvzN>&tbb z>Odv|a4s^Za-we2`A+<0jm4(X(r&^CKnfhvfTVBf)LEH^ru9X>LvHLcB&JM1ucL(HMF=%ekTL9t;1#h^6LIK zIR+nmZ#y&qSYU2#`$vRCgpoqn`~@~qUlJ6eb*QPGP59Vgr~98_rivQGsZ&+!BC35m z$*y)Hm?nzh^Gk`mLfk0U+9N>&Vl*3!jDY~j(OdqOyh%LWQYsjfhtM!24DrYf^i(tA zb;&JnDEAc6E(twGa?P3ny(0THfEC{>K84<1j54+k;_mVPc{WN}`{`i!h%;}n$-EP2 zI{I{`eMH=L5oOLMP<)iGj+OTtS{*IWry{UTxtAPXMjcu1RSgU+!4IC)B{y|8U=LgA zVJvxmhbtiDvyAFo0kBv>%_!fpo{o106#-h!`&YVNMlpYQElkRaN~bp+HK_yc{1&Cq zLbp4VT|z44*C68UEUDTpOcpg=yPm~pr64?4sZo{{#RYI!9qe(eU!=zOmK(+#J8swQ zM09qiqtl7Qz(!{CyRv1I6r{`#z_O0_Q=m9UPR(V5vfxRVbVjPNC7>-idVT}I({+G7 zm>uVTUch|tr9=C6{p5h$rL6hwJfuXE)lX>dV^fU1ow_+o;HiU^`{H$hvu#Y{MGA?> zF9SbaR=oBp{~;w#3C!&AD-)n%`5vcv*+)CQfhHr(>YO%*5`Yw?ssj@VK|xxkIEVIJ zWR8v8H!iO&e;&x#A%2mq6wgV!zbDCX;G>QhC~KAOPvFCH8eI#-OJxgaGiZ(q0shSc z+lCd|`OG@csknlgTkJ8qFNhMSa=njC0e;$OWi3E}gl~3-A(}tm^#u){vrlwloZsIxH-l3Sp)r)r^*-FRYwP!JMQ3yCU$(JvG~4QkNAykF(J zq?)-7l-^LO)-6g}{lTZeRy96CpF&QZ+M#XD-B$r%b4u{nt7F`UKGoK`M&kSo1jg>7 z+0221>spj9un<+YgmIF`KA=x;l#AKanMFm&mSz>WOAelC~4e)_bt=WC%(Z!I}5bC4fu z)yuNl#>9pY{oxQi)6D>zk~Fxo$zuoi8|x%~vA zUoM+-G&8S`jKKf6*Y^M1ZTvqEKu>VGFAukSAdns%P3zZKsAtJj=s7@n06sb#)SV8z zPw^jTLeKxCWS01;U?usgkHL4pp}@LsSqCOUz#=3X3%q6C*x7OT;~;u5;}jG4Ie+@n KseB#thyMW-SGvIf literal 0 HcmV?d00001 diff --git a/lsp4j-mcp/mvnw b/lsp4j-mcp/mvnw new file mode 100644 index 000000000..8d937f4c1 --- /dev/null +++ b/lsp4j-mcp/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/lsp4j-mcp/mvnw.cmd b/lsp4j-mcp/mvnw.cmd new file mode 100644 index 000000000..c4586b564 --- /dev/null +++ b/lsp4j-mcp/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/lsp4j-mcp/pom.xml b/lsp4j-mcp/pom.xml new file mode 100644 index 000000000..40fea12f9 --- /dev/null +++ b/lsp4j-mcp/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + com.redhat.lsp4j + lsp4j-mcp + 0.1.0-SNAPSHOT + jar + + LSP4J MCP Integration + Model Context Protocol (MCP) integration for LSP4J-based language servers + + + 17 + 17 + UTF-8 + 0.24.0 + 2.0.0-M2 + 5.10.0 + + + + + + org.eclipse.lsp4j + org.eclipse.lsp4j + ${lsp4j.version} + + + + + io.modelcontextprotocol.sdk + mcp + ${mcp.version} + + + com.fasterxml.jackson.core + * + + + com.fasterxml.jackson.datatype + * + + + + + io.modelcontextprotocol.sdk + mcp-json-jackson3 + ${mcp.version} + + + + + com.google.code.gson + gson + 2.10.1 + + + + + io.undertow + undertow-servlet + 2.3.17.Final + + + + + org.slf4j + slf4j-api + 2.0.9 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.mockito + mockito-core + 5.5.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/GSonUtils.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/GSonUtils.java new file mode 100644 index 000000000..5331412db --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/GSonUtils.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp; + +import java.util.HashMap; + +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; + +import com.google.gson.Gson; + +/** + * Utilities for JSON serialization of LSP4J objects. + */ +public class GSonUtils { + + private static final Gson LSP4J_GSON = new MessageJsonHandler(new HashMap<>()).getGson(); + + private GSonUtils() { + } + + /** + * Returns a Gson instance configured with all LSP4J type adapters. + * This includes adapters for Position, Range, Diagnostic, CodeAction, etc. + * + * @return a Gson instance configured for LSP4J objects + */ + public static Gson getGson() { + return LSP4J_GSON; + } + + /** + * Converts an object to JSON using LSP4J's Gson configuration. + * + * @param obj the object to serialize + * @return JSON string + */ + public static String toJson(Object obj) { + return LSP4J_GSON.toJson(obj); + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Inject.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Inject.java new file mode 100644 index 000000000..1c6754c5a --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Inject.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field for dependency injection. + * The MCP tool registry will inject the appropriate instance. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Inject { +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/RequireDidOpen.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/RequireDidOpen.java new file mode 100644 index 000000000..74b958bde --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/RequireDidOpen.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a tool requires the file to be opened before execution. + * + * The MCP framework will automatically: + * 1. Check if the file (specified by 'uri' parameter) is already opened + * 2. If not, simulate didOpen with file content from disk + * 3. Execute the tool method + * 4. If file was not originally opened, cleanup with didClose + * + * The tool method must have a parameter named 'uri' of type String. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireDidOpen { + + /** + * Name of the parameter containing the file URI. + * Default is "uri". + */ + String uriParam() default "uri"; + + /** + * Language ID to use when opening the file (e.g., "qute", "java"). + * Default is "qute". + */ + String languageId() default "qute"; +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Tool.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Tool.java new file mode 100644 index 000000000..4b79e9d71 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/Tool.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a method as an MCP tool. + * The method will be automatically registered and exposed via MCP. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Tool { + + /** + * Tool description shown to the MCP client. + */ + String description(); + + /** + * Optional tool name. If not specified, method name is used. + */ + String name() default ""; +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/ToolArg.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/ToolArg.java new file mode 100644 index 000000000..95fc2bb46 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/annotations/ToolArg.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Describes a parameter of an MCP tool method or a getter method of a DTO. + */ +@Target({ElementType.PARAMETER, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ToolArg { + + /** + * Parameter name. If empty, uses the Java parameter name (requires -parameters compiler flag). + */ + String name() default ""; + + /** + * Parameter description shown to the MCP client. + */ + String description(); +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/cache/McpCache.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/cache/McpCache.java new file mode 100644 index 000000000..1510254f0 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/cache/McpCache.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.cache; + +import org.eclipse.lsp4j.Diagnostic; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Cache for MCP server that tracks: + * - Which files are currently opened in the IDE (via didOpen/didClose) + * - Diagnostics for all files (via publishDiagnostics) + * + * This cache allows MCP tools to: + * 1. Know if a file is already opened (to avoid unnecessary didOpen/didClose) + * 2. Retrieve diagnostics without re-parsing + */ +public class McpCache { + + /** + * Set of URIs for files currently opened in the IDE. + * Updated by didOpen/didClose interception. + */ + private final Set openedFiles = Collections.synchronizedSet(new HashSet<>()); + + /** + * Map of URI -> Diagnostics. + * Updated by publishDiagnostics interception. + * Thread-safe because publishDiagnostics can be called asynchronously. + */ + private final Map> diagnostics = new ConcurrentHashMap<>(); + + /** + * Called when a file is opened in the IDE. + * Intercepted from TextDocumentService.didOpen(). + * + * @param uri the file URI + */ + public void onDidOpen(String uri) { + openedFiles.add(uri); + } + + /** + * Called when a file is closed in the IDE. + * Intercepted from TextDocumentService.didClose(). + * + * @param uri the file URI + */ + public void onDidClose(String uri) { + openedFiles.remove(uri); + } + + /** + * Called when diagnostics are published by the language server. + * Intercepted from LanguageClient.publishDiagnostics(). + * + * @param uri the file URI + * @param diagnostics the diagnostics for this file + */ + public void putDiagnostics(String uri, List diagnostics) { + this.diagnostics.put(uri, new ArrayList<>(diagnostics)); + } + + /** + * Check if a file is currently opened in the IDE. + * + * @param uri the file URI + * @return true if the file is opened, false otherwise + */ + public boolean isOpened(String uri) { + return openedFiles.contains(uri); + } + + /** + * Get cached diagnostics for a file. + * + * @param uri the file URI + * @return the diagnostics, or empty list if not in cache + */ + public List getDiagnostics(String uri) { + return diagnostics.getOrDefault(uri, Collections.emptyList()); + } + + /** + * Remove diagnostics from cache. + * Useful for cleanup after temporary didOpen/didClose. + * + * @param uri the file URI + */ + public void removeDiagnostics(String uri) { + diagnostics.remove(uri); + } + + /** + * Clear all cache (for testing or reset). + */ + public void clear() { + openedFiles.clear(); + diagnostics.clear(); + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/LspMcpServer.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/LspMcpServer.java new file mode 100644 index 000000000..3eb5afe4b --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/LspMcpServer.java @@ -0,0 +1,352 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.schema.jackson3.DefaultJsonSchemaValidator; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import io.undertow.Undertow; +import io.undertow.servlet.Servlets; +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.DeploymentManager; +import io.undertow.servlet.api.InstanceFactory; +import io.undertow.servlet.api.InstanceHandle; +import jakarta.servlet.ServletException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tools.jackson.databind.json.JsonMapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Generic MCP (Model Context Protocol) Server for LSP4J-based language servers. + * + * This server exposes language server capabilities as MCP tools via HTTP/SSE, + * allowing AI assistants like Claude Code or Bob to interact with the language server. + * + * Architecture: + *

+ * AI Client (Claude Code, Bob)
+ *   ↕ HTTP/SSE (MCP protocol)
+ * LspMcpServer (Undertow on configurable port)
+ *   ↕ Java API
+ * Language Server (via MCP tools)
+ * 
+ * + * Usage: + *
+ * LspMcpServer mcpServer = LspMcpServer.builder()
+ *     .serverInfo("qute-ls", "1.0.0")
+ *     .port(9339)
+ *     .registerTool(new GetDiagnosticsTool(cache, languageServer))
+ *     .registerTool(new QuteDataModelTool(languageServer))
+ *     .build();
+ *
+ * mcpServer.start();
+ * 
+ */ +public class LspMcpServer { + + private static final Logger LOGGER = LoggerFactory.getLogger(LspMcpServer.class); + + private static final String DEFAULT_SSE_ENDPOINT = "/sse"; + private static final String DEFAULT_MESSAGE_ENDPOINT = "/mcp/message"; + private static final int DEFAULT_PORT = 9339; + + private final String serverName; + private final String serverVersion; + private final int port; + private final List tools; + + private Undertow undertowServer; + private McpSyncServer mcpServer; + private HttpServletSseServerTransportProvider transportProvider; + private boolean started = false; + + private LspMcpServer(Builder builder) { + this.serverName = builder.serverName; + this.serverVersion = builder.serverVersion; + this.port = builder.port; + this.tools = new ArrayList<>(builder.tools); + } + + /** + * Start the MCP server. + * + * The server will listen on http://localhost:{port} with: + * - /sse for SSE connections + * - /mcp/message for client messages + * + * @throws IllegalStateException if server is already started + */ + public void start() { + if (started) { + throw new IllegalStateException("MCP server already started"); + } + + LOGGER.info("Starting MCP Server: {} v{}", serverName, serverVersion); + + try { + // Create Jackson3 JSON mapper + JsonMapper jackson3Mapper = JsonMapper.builder().build(); + JacksonMcpJsonMapper mcpJsonMapper = new JacksonMcpJsonMapper(jackson3Mapper); + + // Create HTTP/SSE transport provider (this is a Servlet) + transportProvider = HttpServletSseServerTransportProvider.builder() + .jsonMapper(mcpJsonMapper) + .messageEndpoint(DEFAULT_MESSAGE_ENDPOINT) + .sseEndpoint(DEFAULT_SSE_ENDPOINT) + .build(); + + // Build MCP server with capabilities and tools + var serverBuilder = io.modelcontextprotocol.server.McpServer.sync(transportProvider) + .serverInfo(serverName, serverVersion) + .jsonMapper(mcpJsonMapper) + .jsonSchemaValidator(new DefaultJsonSchemaValidator()) + .capabilities(McpSchema.ServerCapabilities.builder() + .tools(true) + .build()); + + // Register all tools + LOGGER.info("Registering {} MCP tools", tools.size()); + for (McpToolRegistration toolReg : tools) { + LOGGER.info("Registering tool: {} - {}", toolReg.getName(), toolReg.getDescription()); + + serverBuilder.toolCall( + McpSchema.Tool.builder() + .name(toolReg.getName()) + .description(toolReg.getDescription()) + .inputSchema(toolReg.getInputSchema()) + .build(), + (exchange, request) -> { + try { + return toolReg.getHandler().execute(exchange, request); + } catch (Exception e) { + LOGGER.error("Error executing tool: " + toolReg.getName(), e); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent( + "Error: " + e.getMessage() + ))) + .isError(true) + .build(); + } + } + ); + LOGGER.info("Registered MCP tool: {} - {}", toolReg.getName(), toolReg.getDescription()); + } + + mcpServer = serverBuilder.build(); + + // Configure Undertow servlet deployment + // Use singleton instance to maintain SSE sessions + final HttpServletSseServerTransportProvider singletonServlet = transportProvider; + + DeploymentInfo servletBuilder = Servlets.deployment() + .setClassLoader(LspMcpServer.class.getClassLoader()) + .setContextPath("/") + .setDeploymentName("lsp4j-mcp") + .addServlets( + Servlets.servlet("mcpServlet", HttpServletSseServerTransportProvider.class, + new InstanceFactory() { + @Override + public InstanceHandle createInstance() { + return new InstanceHandle() { + @Override + public HttpServletSseServerTransportProvider getInstance() { + return singletonServlet; + } + + @Override + public void release() { + // Never release - we manage lifecycle + } + }; + } + }) + .addMapping("/*") + .setAsyncSupported(true) + ); + + DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder); + manager.deploy(); + + // Create and start Undertow server + undertowServer = Undertow.builder() + .addHttpListener(port, "localhost") + .setHandler(manager.start()) + .build(); + + undertowServer.start(); + + started = true; + LOGGER.info("MCP Server started successfully"); + LOGGER.info(" SSE endpoint: http://localhost:{}{}", port, DEFAULT_SSE_ENDPOINT); + LOGGER.info(" Message endpoint: http://localhost:{}{}", port, DEFAULT_MESSAGE_ENDPOINT); + + } catch (ServletException e) { + LOGGER.error("Failed to deploy MCP servlet", e); + throw new RuntimeException("Failed to start MCP server", e); + } catch (Exception e) { + LOGGER.error("Failed to start MCP server", e); + throw new RuntimeException("Failed to start MCP server", e); + } + } + + /** + * Stop the MCP server. + */ + public void stop() { + if (!started) { + return; + } + + LOGGER.info("Stopping MCP Server"); + try { + if (undertowServer != null) { + undertowServer.stop(); + } + if (transportProvider != null) { + transportProvider.close(); + } + if (mcpServer != null) { + mcpServer.close(); + } + started = false; + LOGGER.info("MCP Server stopped"); + } catch (Exception e) { + LOGGER.error("Error stopping MCP server", e); + } + } + + /** + * Check if the server is running. + */ + public boolean isStarted() { + return started; + } + + /** + * Create a new builder for LspMcpServer. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for LspMcpServer. + */ + public static class Builder { + private String serverName = "lsp4j-mcp-server"; + private String serverVersion = "1.0.0"; + private int port = DEFAULT_PORT; + private final McpToolRegistry registry = new McpToolRegistry(); + private final List tools = new ArrayList<>(); + + /** + * Set the server name and version. + */ + public Builder serverInfo(String name, String version) { + this.serverName = name; + this.serverVersion = version; + return this; + } + + /** + * Set the port to listen on. + */ + public Builder port(int port) { + this.port = port; + return this; + } + + /** + * Register a dependency for injection into tool providers. + */ + public Builder registerDependency(Class type, Object instance) { + registry.register(type, instance); + return this; + } + + /** + * Register an MCP tool manually (legacy support). + * @deprecated Use annotation-based tools with SPI instead + */ + @Deprecated + public Builder registerTool(String name, String description, Map inputSchema, McpToolHandler handler) { + this.tools.add(new McpToolRegistration(name, description, inputSchema, handler)); + return this; + } + + /** + * Build the LspMcpServer. + */ + public LspMcpServer build() { + // Auto-discover tool classes via SPI + loadToolsFromSPI(); + + // Add discovered tools to the tools list + tools.addAll(registry.getToolRegistrations()); + + return new LspMcpServer(this); + } + + private void loadToolsFromSPI() { + // Use ServiceLoader to discover MCP tools + ServiceLoader loader = + ServiceLoader.load(com.redhat.lsp4j.mcp.tools.McpTool.class); + + for (com.redhat.lsp4j.mcp.tools.McpTool tool : loader) { + LOGGER.info("Discovered MCP tool: {}", tool.getClass().getName()); + registry.scanAndRegister(tool); + } + } + } + + /** + * Tool registration. + */ + public static class McpToolRegistration { + private final String name; + private final String description; + private final Map inputSchema; + private final McpToolHandler handler; + + McpToolRegistration(String name, String description, Map inputSchema, McpToolHandler handler) { + this.name = name; + this.description = description; + this.inputSchema = inputSchema; + this.handler = handler; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Map getInputSchema() { + return inputSchema; + } + + public McpToolHandler getHandler() { + return handler; + } + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageClientWrapper.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageClientWrapper.java new file mode 100644 index 000000000..426b2ea53 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageClientWrapper.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import com.redhat.lsp4j.mcp.cache.McpCache; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.services.LanguageClient; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Generic wrapper for LanguageClient that intercepts publishDiagnostics + * to cache diagnostics for MCP tools. + * + * Uses dynamic proxy to avoid implementing all interface methods manually. + * Works with any LanguageClient implementation. + * + * @param The specific LanguageClient type to wrap + */ +public class McpLanguageClientWrapper { + + /** + * Create a wrapper for a LanguageClient that intercepts publishDiagnostics. + * + * @param The specific LanguageClient type + * @param clientClass The LanguageClient class + * @param delegate The LanguageClient instance to wrap + * @param cache The MCP cache for storing diagnostics + * @return A wrapped LanguageClient instance + */ + @SuppressWarnings("unchecked") + public static C wrap(Class clientClass, C delegate, McpCache cache) { + return (C) Proxy.newProxyInstance( + clientClass.getClassLoader(), + new Class[]{clientClass}, + new McpInvocationHandler<>(delegate, cache) + ); + } + + private static class McpInvocationHandler implements InvocationHandler { + private final C delegate; + private final McpCache cache; + + McpInvocationHandler(C delegate, McpCache cache) { + this.delegate = delegate; + this.cache = cache; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Intercept publishDiagnostics + if (method.getName().equals("publishDiagnostics") && args != null && args.length == 1) { + PublishDiagnosticsParams diagnostics = (PublishDiagnosticsParams) args[0]; + cache.putDiagnostics(diagnostics.getUri(), diagnostics.getDiagnostics()); + } + + // Delegate to the real client + return method.invoke(delegate, args); + } + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageServerWrapper.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageServerWrapper.java new file mode 100644 index 000000000..d747e0d13 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpLanguageServerWrapper.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import com.redhat.lsp4j.mcp.cache.McpCache; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Generic wrapper for LanguageServer that wraps the TextDocumentService + * to intercept didOpen/didClose for MCP caching. + * + * Uses dynamic proxy to avoid implementing all interface methods manually. + * Works with any LanguageServer implementation. + * + * @param The specific LanguageServer type to wrap + */ +public class McpLanguageServerWrapper { + + /** + * Create a wrapper for a LanguageServer that wraps TextDocumentService. + * + * @param The specific LanguageServer type + * @param serverClass The LanguageServer class + * @param delegate The LanguageServer instance to wrap + * @param cache The MCP cache for tracking file state + * @return A wrapped LanguageServer instance + */ + @SuppressWarnings("unchecked") + public static S wrap(Class serverClass, S delegate, McpCache cache) { + McpTextDocumentServiceWrapper wrappedTextDocService = new McpTextDocumentServiceWrapper( + delegate.getTextDocumentService(), + cache + ); + + return (S) Proxy.newProxyInstance( + serverClass.getClassLoader(), + new Class[]{serverClass}, + new McpInvocationHandler<>(delegate, wrappedTextDocService) + ); + } + + private static class McpInvocationHandler implements InvocationHandler { + private final S delegate; + private final McpTextDocumentServiceWrapper wrappedTextDocService; + + McpInvocationHandler(S delegate, McpTextDocumentServiceWrapper wrappedTextDocService) { + this.delegate = delegate; + this.wrappedTextDocService = wrappedTextDocService; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Intercept getTextDocumentService to return wrapped version + if (method.getName().equals("getTextDocumentService") && + TextDocumentService.class.isAssignableFrom(method.getReturnType())) { + return wrappedTextDocService; + } + + // Delegate all other methods to the real server + return method.invoke(delegate, args); + } + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpTextDocumentServiceWrapper.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpTextDocumentServiceWrapper.java new file mode 100644 index 000000000..aa8a6a66b --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpTextDocumentServiceWrapper.java @@ -0,0 +1,360 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.CallHierarchyIncomingCall; +import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; +import org.eclipse.lsp4j.CallHierarchyItem; +import org.eclipse.lsp4j.CallHierarchyOutgoingCall; +import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams; +import org.eclipse.lsp4j.CallHierarchyPrepareParams; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.CodeLensParams; +import org.eclipse.lsp4j.ColorInformation; +import org.eclipse.lsp4j.ColorPresentation; +import org.eclipse.lsp4j.ColorPresentationParams; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentColorParams; +import org.eclipse.lsp4j.DocumentDiagnosticParams; +import org.eclipse.lsp4j.DocumentDiagnosticReport; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightParams; +import org.eclipse.lsp4j.DocumentLink; +import org.eclipse.lsp4j.DocumentLinkParams; +import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; +import org.eclipse.lsp4j.DocumentSymbol; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.FoldingRange; +import org.eclipse.lsp4j.FoldingRangeRequestParams; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.ImplementationParams; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.InlineValue; +import org.eclipse.lsp4j.InlineValueParams; +import org.eclipse.lsp4j.LinkedEditingRangeParams; +import org.eclipse.lsp4j.LinkedEditingRanges; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Moniker; +import org.eclipse.lsp4j.MonikerParams; +import org.eclipse.lsp4j.PrepareRenameDefaultBehavior; +import org.eclipse.lsp4j.PrepareRenameParams; +import org.eclipse.lsp4j.PrepareRenameResult; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.ReferenceParams; +import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.SelectionRange; +import org.eclipse.lsp4j.SelectionRangeParams; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensDelta; +import org.eclipse.lsp4j.SemanticTokensDeltaParams; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.eclipse.lsp4j.SemanticTokensRangeParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.TypeDefinitionParams; +import org.eclipse.lsp4j.TypeHierarchyItem; +import org.eclipse.lsp4j.TypeHierarchyPrepareParams; +import org.eclipse.lsp4j.TypeHierarchySubtypesParams; +import org.eclipse.lsp4j.TypeHierarchySupertypesParams; +import org.eclipse.lsp4j.WillSaveTextDocumentParams; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Either3; +import org.eclipse.lsp4j.services.TextDocumentService; + +import com.redhat.lsp4j.mcp.cache.McpCache; + +/** + * Wrapper for TextDocumentService that intercepts didOpen/didClose to track + * which files are opened in the IDE. + * + * All other methods are delegated to the wrapped service. + */ +public class McpTextDocumentServiceWrapper implements TextDocumentService { + + private final TextDocumentService delegate; + private final McpCache cache; + + public McpTextDocumentServiceWrapper(TextDocumentService delegate, McpCache cache) { + this.delegate = delegate; + this.cache = cache; + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + // Track that this file is now opened + cache.onDidOpen(params.getTextDocument().getUri()); + + // Forward to the real language server + delegate.didOpen(params); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + // Track that this file is now closed + cache.onDidClose(params.getTextDocument().getUri()); + + // Forward to the real language server + delegate.didClose(params); + } + + // All methods below are simple delegation - no interception needed + + @Override + public void didChange(DidChangeTextDocumentParams params) { + delegate.didChange(params); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + delegate.didSave(params); + } + + @Override + public CompletableFuture, CompletionList>> completion(CompletionParams params) { + return delegate.completion(params); + } + + @Override + public CompletableFuture resolveCompletionItem(CompletionItem unresolved) { + return delegate.resolveCompletionItem(unresolved); + } + + @Override + public CompletableFuture hover(HoverParams params) { + return delegate.hover(params); + } + + @Override + public CompletableFuture signatureHelp(SignatureHelpParams params) { + return delegate.signatureHelp(params); + } + + @Override + public CompletableFuture, List>> definition( + DefinitionParams params) { + return delegate.definition(params); + } + + @Override + public CompletableFuture> references(ReferenceParams params) { + return delegate.references(params); + } + + @Override + public CompletableFuture> documentHighlight(DocumentHighlightParams params) { + return delegate.documentHighlight(params); + } + + @Override + public CompletableFuture>> documentSymbol( + DocumentSymbolParams params) { + return delegate.documentSymbol(params); + } + + @Override + public CompletableFuture>> codeAction(CodeActionParams params) { + return delegate.codeAction(params); + } + + @Override + public CompletableFuture resolveCodeAction(CodeAction unresolved) { + return delegate.resolveCodeAction(unresolved); + } + + @Override + public CompletableFuture> codeLens(CodeLensParams params) { + return delegate.codeLens(params); + } + + @Override + public CompletableFuture resolveCodeLens(CodeLens unresolved) { + return delegate.resolveCodeLens(unresolved); + } + + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + return delegate.formatting(params); + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + return delegate.rangeFormatting(params); + } + + @Override + public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams params) { + return delegate.onTypeFormatting(params); + } + + @Override + public CompletableFuture rename(RenameParams params) { + return delegate.rename(params); + } + + @Override + public CompletableFuture linkedEditingRange(LinkedEditingRangeParams params) { + return delegate.linkedEditingRange(params); + } + + @Override + public void willSave(WillSaveTextDocumentParams params) { + delegate.willSave(params); + } + + @Override + public CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams params) { + return delegate.willSaveWaitUntil(params); + } + + @Override + public CompletableFuture, List>> typeDefinition( + TypeDefinitionParams params) { + return delegate.typeDefinition(params); + } + + @Override + public CompletableFuture, List>> implementation( + ImplementationParams params) { + return delegate.implementation(params); + } + + @Override + public CompletableFuture> documentColor(DocumentColorParams params) { + return delegate.documentColor(params); + } + + @Override + public CompletableFuture> colorPresentation(ColorPresentationParams params) { + return delegate.colorPresentation(params); + } + + @Override + public CompletableFuture> foldingRange(FoldingRangeRequestParams params) { + return delegate.foldingRange(params); + } + + @Override + public CompletableFuture> prepareRename( + PrepareRenameParams params) { + return delegate.prepareRename(params); + } + + @Override + public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams params) { + return delegate.prepareCallHierarchy(params); + } + + @Override + public CompletableFuture> callHierarchyIncomingCalls( + CallHierarchyIncomingCallsParams params) { + return delegate.callHierarchyIncomingCalls(params); + } + + @Override + public CompletableFuture> callHierarchyOutgoingCalls( + CallHierarchyOutgoingCallsParams params) { + return delegate.callHierarchyOutgoingCalls(params); + } + + @Override + public CompletableFuture> selectionRange(SelectionRangeParams params) { + return delegate.selectionRange(params); + } + + @Override + public CompletableFuture> documentLink(DocumentLinkParams params) { + return delegate.documentLink(params); + } + + @Override + public CompletableFuture documentLinkResolve(DocumentLink params) { + return delegate.documentLinkResolve(params); + } + + @Override + public CompletableFuture> prepareTypeHierarchy(TypeHierarchyPrepareParams params) { + return delegate.prepareTypeHierarchy(params); + } + + @Override + public CompletableFuture> typeHierarchySupertypes(TypeHierarchySupertypesParams params) { + return delegate.typeHierarchySupertypes(params); + } + + @Override + public CompletableFuture> typeHierarchySubtypes(TypeHierarchySubtypesParams params) { + return delegate.typeHierarchySubtypes(params); + } + + @Override + public CompletableFuture> inlayHint(InlayHintParams params) { + return delegate.inlayHint(params); + } + + @Override + public CompletableFuture resolveInlayHint(InlayHint unresolved) { + return delegate.resolveInlayHint(unresolved); + } + + @Override + public CompletableFuture> inlineValue(InlineValueParams params) { + return delegate.inlineValue(params); + } + + @Override + public CompletableFuture> moniker(MonikerParams params) { + return delegate.moniker(params); + } + + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + return delegate.semanticTokensFull(params); + } + + @Override + public CompletableFuture> semanticTokensFullDelta( + SemanticTokensDeltaParams params) { + return delegate.semanticTokensFullDelta(params); + } + + @Override + public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { + return delegate.semanticTokensRange(params); + } + + @Override + public CompletableFuture diagnostic(DocumentDiagnosticParams params) { + return delegate.diagnostic(params); + } + +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolHandler.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolHandler.java new file mode 100644 index 000000000..d06141629 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolHandler.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import io.modelcontextprotocol.server.McpSyncServerExchange; +import io.modelcontextprotocol.spec.McpSchema; + +/** + * Functional interface for MCP tool handlers. + */ +@FunctionalInterface +public interface McpToolHandler { + + /** + * Execute the tool with the given request. + * + * @param exchange the MCP server exchange + * @param request the tool call request + * @return the tool result + */ + McpSchema.CallToolResult execute(McpSyncServerExchange exchange, McpSchema.CallToolRequest request); +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolRegistry.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolRegistry.java new file mode 100644 index 000000000..dbb472d12 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/server/McpToolRegistry.java @@ -0,0 +1,445 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.services.LanguageServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.redhat.lsp4j.mcp.GSonUtils; +import com.redhat.lsp4j.mcp.annotations.Inject; +import com.redhat.lsp4j.mcp.annotations.RequireDidOpen; +import com.redhat.lsp4j.mcp.annotations.Tool; +import com.redhat.lsp4j.mcp.annotations.ToolArg; +import com.redhat.lsp4j.mcp.cache.McpCache; + +import io.modelcontextprotocol.server.McpSyncServerExchange; +import io.modelcontextprotocol.spec.McpSchema; + +/** + * Registry for MCP tools with dependency injection and automatic registration. + */ +public class McpToolRegistry { + + private static final Logger LOGGER = LoggerFactory.getLogger(McpToolRegistry.class); + + private final Map, Object> dependencies = new HashMap<>(); + private final List toolRegistrations = new ArrayList<>(); + + /** + * Register a dependency for injection. + */ + public void register(Class type, Object instance) { + dependencies.put(type, instance); + } + + /** + * Inject dependencies into a tool class instance and scan its @Tool methods. + */ + public void scanAndRegister(Object toolInstance) { + // Inject dependencies + injectDependencies(toolInstance); + + // Scan @Tool methods + for (Method method : toolInstance.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(Tool.class)) { + registerTool(toolInstance, method); + } + } + } + + /** + * Get all registered tools. + */ + public List getToolRegistrations() { + return toolRegistrations; + } + + private void injectDependencies(Object instance) { + for (Field field : instance.getClass().getDeclaredFields()) { + if (field.isAnnotationPresent(Inject.class)) { + Object dependency = dependencies.get(field.getType()); + if (dependency == null) { + throw new IllegalStateException("No dependency registered for type: " + field.getType()); + } + field.setAccessible(true); + try { + field.set(instance, dependency); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to inject dependency: " + field.getName(), e); + } + } + } + } + + private void registerTool(Object toolInstance, Method method) { + Tool toolAnnotation = method.getAnnotation(Tool.class); + + // Determine tool name + String toolName = toolAnnotation.name(); + if (toolName.isEmpty()) { + toolName = camelToSnake(method.getName()); + } + + String description = toolAnnotation.description(); + + // Generate input schema + Map inputSchema = generateInputSchema(method); + + // Create handler + McpToolHandler handler = createHandler(toolInstance, method); + + LOGGER.info("Registered tool: {} from method {}.{}", toolName, + toolInstance.getClass().getSimpleName(), method.getName()); + + toolRegistrations.add(new LspMcpServer.McpToolRegistration(toolName, description, inputSchema, handler)); + } + + public Map generateInputSchema(Method method) { + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + + for (Parameter param : method.getParameters()) { + // Get parameter name from @ToolArg annotation if present, otherwise use Java name (arg0, arg1, etc.) + String paramName = param.getName(); + ToolArg paramAnnotation = param.getAnnotation(ToolArg.class); + if (paramAnnotation != null && !paramAnnotation.name().isEmpty()) { + paramName = paramAnnotation.name(); + } + + // Check if parameter type has @ToolArg on its methods (like Position) + if (hasToolArgAnnotations(param.getType())) { + // Nested object - recurse into its methods + Map nestedProps = new LinkedHashMap<>(); + List nestedRequired = new ArrayList<>(); + for (Method getter : param.getType().getDeclaredMethods()) { + if (getter.isAnnotationPresent(ToolArg.class)) { + ToolArg argAnnotation = getter.getAnnotation(ToolArg.class); + String fieldName = getterToFieldName(getter.getName()); + nestedProps.put(fieldName, Map.of( + "type", javaTypeToJsonType(getter.getReturnType()), + "description", argAnnotation.description() + )); + nestedRequired.add(fieldName); + } + } + properties.put(paramName, Map.of( + "type", "object", + "properties", nestedProps, + "required", nestedRequired + )); + required.add(paramName); + } else { + // Simple parameter + ToolArg argAnnotation = param.getAnnotation(ToolArg.class); + String description = argAnnotation != null ? argAnnotation.description() : ""; + + properties.put(paramName, Map.of( + "type", javaTypeToJsonType(param.getType()), + "description", description + )); + + required.add(paramName); + } + } + + return Map.of( + "type", "object", + "properties", properties, + "required", required + ); + } + + private boolean hasToolArgAnnotations(Class type) { + for (Method method : type.getDeclaredMethods()) { + if (method.isAnnotationPresent(ToolArg.class)) { + return true; + } + } + return false; + } + + private String getterToFieldName(String getterName) { + if (getterName.startsWith("get") && getterName.length() > 3) { + return Character.toLowerCase(getterName.charAt(3)) + getterName.substring(4); + } + return getterName; + } + + private String javaTypeToJsonType(Class type) { + if (type == String.class) return "string"; + if (type == int.class || type == Integer.class) return "integer"; + if (type == long.class || type == Long.class) return "integer"; + if (type == double.class || type == Double.class) return "number"; + if (type == float.class || type == Float.class) return "number"; + if (type == boolean.class || type == Boolean.class) return "boolean"; + return "object"; + } + + private String camelToSnake(String camelCase) { + return camelCase.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase(); + } + + private McpToolHandler createHandler(Object toolInstance, Method method) { + RequireDidOpen requireDidOpen = method.getAnnotation(RequireDidOpen.class); + + if (requireDidOpen != null) { + return new DidOpenInterceptorHandler(toolInstance, method, requireDidOpen); + } else { + return new SimpleMethodHandler(toolInstance, method); + } + } + + /** + * Simple handler that just invokes the method. + */ + private static class SimpleMethodHandler implements McpToolHandler { + private final Object toolInstance; + private final Method method; + + SimpleMethodHandler(Object toolInstance, Method method) { + this.toolInstance = toolInstance; + this.method = method; + } + + @Override + public McpSchema.CallToolResult execute(McpSyncServerExchange exchange, McpSchema.CallToolRequest request) { + try { + Map args = request.arguments(); + Object[] methodArgs = extractMethodArgs(method, args); + + Object result = method.invoke(toolInstance, methodArgs); + + String json = GSonUtils.toJson(result); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(json))) + .isError(false) + .build(); + + } catch (Exception e) { + LOGGER.error("Error executing tool", e); + return errorResult(e.getMessage()); + } + } + + private Object[] extractMethodArgs(Method method, Map args) { + Parameter[] params = method.getParameters(); + Object[] methodArgs = new Object[params.length]; + + for (int i = 0; i < params.length; i++) { + Parameter param = params[i]; + // Use same logic as generateInputSchema to get parameter name + String paramName = param.getName(); + ToolArg paramAnnotation = param.getAnnotation(ToolArg.class); + if (paramAnnotation != null && !paramAnnotation.name().isEmpty()) { + paramName = paramAnnotation.name(); + } + Object value = args.get(paramName); + + // If value is null but param is not a primitive, try to construct from args + if (value == null && !param.getType().isPrimitive()) { + // Try to deserialize the whole args map into the parameter type + // This handles cases where MCP client sends flat structure but we expect nested objects + try { + value = GSonUtils.getGson().fromJson( + GSonUtils.getGson().toJson(args), + param.getType() + ); + } catch (Exception e) { + // If that fails, leave it null + } + } else if (value instanceof Map && !param.getType().equals(Map.class)) { + // Handle nested objects like Position when explicitly provided + value = GSonUtils.getGson().fromJson( + GSonUtils.getGson().toJson(value), + param.getType() + ); + } + + methodArgs[i] = value; + } + + return methodArgs; + } + + private McpSchema.CallToolResult errorResult(String message) { + Map error = Map.of("error", message); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(GSonUtils.toJson(error)))) + .isError(true) + .build(); + } + } + + /** + * Handler that wraps method invocation with didOpen/didClose logic. + */ + private class DidOpenInterceptorHandler implements McpToolHandler { + private final Object toolInstance; + private final Method method; + private final RequireDidOpen annotation; + + DidOpenInterceptorHandler(Object toolInstance, Method method, RequireDidOpen annotation) { + this.toolInstance = toolInstance; + this.method = method; + this.annotation = annotation; + } + + @Override + public McpSchema.CallToolResult execute(McpSyncServerExchange exchange, McpSchema.CallToolRequest request) { + try { + Map args = request.arguments(); + String uri = extractUri(args); + + McpCache cache = (McpCache) dependencies.get(McpCache.class); + LanguageServer languageServer = (LanguageServer) dependencies.get(LanguageServer.class); + + boolean wasOpened = cache.isOpened(uri); + + // If file not opened, simulate didOpen + if (!wasOpened) { + LOGGER.info("File not opened, simulating didOpen: {}", uri); + + String content; + try { + String path = uri.replace("file:///", "").replace("file://", ""); + content = Files.readString(Paths.get(path)); + } catch (Exception e) { + return errorResult("Failed to read file: " + e.getMessage()); + } + + DidOpenTextDocumentParams didOpenParams = new DidOpenTextDocumentParams(); + TextDocumentItem textDocument = new TextDocumentItem(); + textDocument.setUri(uri); + textDocument.setLanguageId(annotation.languageId()); + textDocument.setVersion(1); + textDocument.setText(content); + didOpenParams.setTextDocument(textDocument); + + languageServer.getTextDocumentService().didOpen(didOpenParams); + + // Wait for publishDiagnostics to be called + // TODO: Use a proper notification mechanism instead of sleep + Thread.sleep(500); + } + + // Execute the actual tool method + Object[] methodArgs = extractMethodArgs(method, args); + Object result = method.invoke(toolInstance, methodArgs); + + // Cleanup if needed + if (!wasOpened) { + LOGGER.info("Cleaning up with didClose: {}", uri); + DidCloseTextDocumentParams didCloseParams = new DidCloseTextDocumentParams(); + TextDocumentIdentifier textDocument = new TextDocumentIdentifier(uri); + didCloseParams.setTextDocument(textDocument); + languageServer.getTextDocumentService().didClose(didCloseParams); + } + + String json = GSonUtils.toJson(result); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(json))) + .isError(false) + .build(); + + } catch (Exception e) { + LOGGER.error("Error executing tool with didOpen interception", e); + return errorResult(e.getMessage()); + } + } + + private String extractUri(Map args) { + String uriParamName = annotation.uriParam(); + + // Support nested paths like "textDocument.uri" + String[] parts = uriParamName.split("\\."); + Object current = args; + + for (String part : parts) { + if (current instanceof Map) { + current = ((Map) current).get(part); + } else { + throw new IllegalArgumentException("Cannot navigate to " + uriParamName); + } + + if (current == null) { + throw new IllegalArgumentException("Missing required uri parameter: " + uriParamName); + } + } + + return current.toString(); + } + + public Object[] extractMethodArgs(Method method, Map args) { + Parameter[] params = method.getParameters(); + Object[] methodArgs = new Object[params.length]; + + for (int i = 0; i < params.length; i++) { + Parameter param = params[i]; + // Use same logic as generateInputSchema to get parameter name + String paramName = param.getName(); + ToolArg paramAnnotation = param.getAnnotation(ToolArg.class); + if (paramAnnotation != null && !paramAnnotation.name().isEmpty()) { + paramName = paramAnnotation.name(); + } + Object value = args.get(paramName); + + // If value is null but param is not a primitive, try to construct from args + if (value == null && !param.getType().isPrimitive()) { + // Try to deserialize the whole args map into the parameter type + // This handles cases where MCP client sends flat structure but we expect nested objects + try { + value = GSonUtils.getGson().fromJson( + GSonUtils.getGson().toJson(args), + param.getType() + ); + } catch (Exception e) { + // If that fails, leave it null + } + } else if (value instanceof Map && !param.getType().equals(Map.class)) { + // Handle nested objects like Position when explicitly provided + value = GSonUtils.getGson().fromJson( + GSonUtils.getGson().toJson(value), + param.getType() + ); + } + + methodArgs[i] = value; + } + + return methodArgs; + } + + private McpSchema.CallToolResult errorResult(String message) { + Map error = Map.of("error", message); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(GSonUtils.toJson(error)))) + .isError(true) + .build(); + } + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/McpTool.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/McpTool.java new file mode 100644 index 000000000..e7ae21d23 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/McpTool.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.tools; + +/** + * Marker interface for MCP tools discovered via SPI. + * + * Classes implementing this interface will be automatically discovered + * and their @Tool annotated methods will be registered as MCP tools. + * + * This interface has no methods - it's purely for ServiceLoader discovery. + */ +public interface McpTool { +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/Position.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/Position.java new file mode 100644 index 000000000..ca37e2a0b --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/Position.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.tools; + +import com.redhat.lsp4j.mcp.annotations.ToolArg; + +/** + * Position with MCP annotations for automatic schema generation. + * Note: We don't extend LSP4J Position because Gson needs direct field access for deserialization. + */ +public class Position { + + private int line; + private int character; + + public Position() { + } + + public Position(int line, int character) { + this.line = line; + this.character = character; + } + + @ToolArg(description = "Line number (0-based)") + public int getLine() { + return line; + } + + public void setLine(int line) { + this.line = line; + } + + @ToolArg(description = "Character offset in line (0-based)") + public int getCharacter() { + return character; + } + + public void setCharacter(int character) { + this.character = character; + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/TextDocumentIdentifier.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/TextDocumentIdentifier.java new file mode 100644 index 000000000..76962db82 --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/TextDocumentIdentifier.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.tools; + +import com.redhat.lsp4j.mcp.annotations.ToolArg; + +/** + * TextDocumentIdentifier with MCP annotations for automatic schema generation. + * Note: We don't extend LSP4J TextDocumentIdentifier because Gson needs direct field access. + */ +public class TextDocumentIdentifier { + + private String uri; + + public TextDocumentIdentifier() { + } + + public TextDocumentIdentifier(String uri) { + this.uri = uri; + } + + @ToolArg(description = "File URI (e.g., file:///path/to/file)") + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetCodeActionsTool.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetCodeActionsTool.java new file mode 100644 index 000000000..e5e22712f --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetCodeActionsTool.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.tools.lsp; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageServer; + +import com.redhat.lsp4j.mcp.annotations.Inject; +import com.redhat.lsp4j.mcp.annotations.RequireDidOpen; +import com.redhat.lsp4j.mcp.annotations.Tool; +import com.redhat.lsp4j.mcp.annotations.ToolArg; +import com.redhat.lsp4j.mcp.cache.McpCache; +import com.redhat.lsp4j.mcp.tools.McpTool; +import com.redhat.lsp4j.mcp.tools.Position; +import com.redhat.lsp4j.mcp.tools.TextDocumentIdentifier; + +/** + * Generic LSP tool to get code actions at a position. Works with any + * LSP4J-based language server. + */ +public class LspGetCodeActionsTool implements McpTool { + + /** + * Request object for code action tool. + */ + public static class CodeActionRequest { + + private TextDocumentIdentifier textDocument; + private Position position; + + public CodeActionRequest() { + } + + public CodeActionRequest(TextDocumentIdentifier textDocument, Position position) { + this.textDocument = textDocument; + this.position = position; + } + + @ToolArg(description = "Text document identifier") + public TextDocumentIdentifier getTextDocument() { + return textDocument; + } + + public void setTextDocument(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } + + @ToolArg(description = "Position in the file") + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + } + + @Inject + private McpCache cache; + + @Inject + private LanguageServer languageServer; + + @Tool(description = "Get code actions at a given position in a file") + @RequireDidOpen(uriParam = "arg0.textDocument.uri") + public List> getCodeActions(CodeActionRequest request) throws Exception { + Position position = request.getPosition(); + String uri = request.getTextDocument().getUri(); + + // Get diagnostics at this position + List allDiagnostics = cache.getDiagnostics(uri); + List diagnosticsAtPosition = filterDiagnosticsAtPosition(allDiagnostics, position); + + // Build CodeActionParams + CodeActionParams params = new CodeActionParams(); + params.setTextDocument(new org.eclipse.lsp4j.TextDocumentIdentifier(uri)); + org.eclipse.lsp4j.Position lspPosition = new org.eclipse.lsp4j.Position(position.getLine(), position.getCharacter()); + params.setRange(new Range(lspPosition, lspPosition)); + + CodeActionContext context = new CodeActionContext(); + context.setDiagnostics(diagnosticsAtPosition); + params.setContext(context); + + // Call textDocument/codeAction + return languageServer.getTextDocumentService().codeAction(params).get(5, TimeUnit.SECONDS); + } + + private List filterDiagnosticsAtPosition(List diagnostics, Position position) { + if (diagnostics == null) { + return new ArrayList<>(); + } + + return diagnostics.stream().filter(d -> isPositionInRange(position, d.getRange())).collect(Collectors.toList()); + } + + private boolean isPositionInRange(Position position, Range range) { + if (range == null) { + return false; + } + + org.eclipse.lsp4j.Position start = range.getStart(); + org.eclipse.lsp4j.Position end = range.getEnd(); + + // Check if position is after or at start + if (position.getLine() < start.getLine()) { + return false; + } + if (position.getLine() == start.getLine() && position.getCharacter() < start.getCharacter()) { + return false; + } + + // Check if position is before or at end + if (position.getLine() > end.getLine()) { + return false; + } + if (position.getLine() == end.getLine() && position.getCharacter() > end.getCharacter()) { + return false; + } + + return true; + } +} diff --git a/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetDiagnosticsTool.java b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetDiagnosticsTool.java new file mode 100644 index 000000000..acbc0e36c --- /dev/null +++ b/lsp4j-mcp/src/main/java/com/redhat/lsp4j/mcp/tools/lsp/LspGetDiagnosticsTool.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.tools.lsp; + +import java.util.List; + +import org.eclipse.lsp4j.Diagnostic; + +import com.redhat.lsp4j.mcp.annotations.Inject; +import com.redhat.lsp4j.mcp.annotations.RequireDidOpen; +import com.redhat.lsp4j.mcp.annotations.Tool; +import com.redhat.lsp4j.mcp.annotations.ToolArg; +import com.redhat.lsp4j.mcp.cache.McpCache; +import com.redhat.lsp4j.mcp.tools.McpTool; +import com.redhat.lsp4j.mcp.tools.TextDocumentIdentifier; + +/** + * Generic LSP tool to get diagnostics for a file. + * Works with any LSP4J-based language server. + */ +public class LspGetDiagnosticsTool implements McpTool { + + @Inject + private McpCache cache; + + @Tool(description = "Get diagnostics for a file") + @RequireDidOpen(uriParam = "textDocument.uri") + public List getDiagnostics( + @ToolArg(name = "textDocument", description = "Text document identifier") TextDocumentIdentifier textDocument) { + return cache.getDiagnostics(textDocument.getUri()); + } +} diff --git a/lsp4j-mcp/src/main/resources/META-INF/services/com.redhat.lsp4j.mcp.tools.McpTool b/lsp4j-mcp/src/main/resources/META-INF/services/com.redhat.lsp4j.mcp.tools.McpTool new file mode 100644 index 000000000..066425007 --- /dev/null +++ b/lsp4j-mcp/src/main/resources/META-INF/services/com.redhat.lsp4j.mcp.tools.McpTool @@ -0,0 +1,3 @@ +# MCP Tool classes - no interface required, just annotate methods with @Tool +com.redhat.lsp4j.mcp.tools.lsp.LspGetDiagnosticsTool +com.redhat.lsp4j.mcp.tools.lsp.LspGetCodeActionsTool diff --git a/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpJsonSchemaGenerationTest.java b/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpJsonSchemaGenerationTest.java new file mode 100644 index 000000000..916cea68d --- /dev/null +++ b/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpJsonSchemaGenerationTest.java @@ -0,0 +1,357 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.redhat.lsp4j.mcp.annotations.Tool; +import com.redhat.lsp4j.mcp.annotations.ToolArg; +import com.redhat.lsp4j.mcp.tools.McpTool; + +/** + * Test JSON Schema generation from @Tool and @ToolArg annotations. + * Compares generated schemas with expected JSON to see exactly what MCP clients receive. + */ +class McpJsonSchemaGenerationTest { + + private McpToolRegistry registry; + private Gson gson; + + @BeforeEach + void setUp() { + registry = new McpToolRegistry(); + gson = new GsonBuilder().setPrettyPrinting().create(); + } + + @Test + void testSimpleStringParameter_withName() throws Exception { + class SimpleTool implements McpTool { + @Tool(description = "A simple tool") + public String execute(@ToolArg(name = "input", description = "Input string") String input) { + return input; + } + } + + String actualJson = generateSchemaJson(SimpleTool.class, "execute", String.class); + + String expectedJson = """ + { + "type": "object", + "properties": { + "input": { + "type": "string", + "description": "Input string" + } + }, + "required": [ + "input" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + @Test + void testSimpleParameter_withoutName_generatesArg0() throws Exception { + class SimpleTool implements McpTool { + @Tool(description = "Test tool") + public String execute(String unnamed) { + return unnamed; + } + } + + String actualJson = generateSchemaJson(SimpleTool.class, "execute", String.class); + + String expectedJson = """ + { + "type": "object", + "properties": { + "arg0": { + "type": "string", + "description": "" + } + }, + "required": [ + "arg0" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + @Test + void testNestedObject_withToolArgOnGetters() throws Exception { + class NestedDTO { + @ToolArg(description = "The URI") + public String getUri() { + return null; + } + + @ToolArg(description = "The line number") + public int getLine() { + return 0; + } + } + + class ToolWithNested implements McpTool { + @Tool(description = "Tool with nested object") + public void execute(@ToolArg(name = "request", description = "The request") NestedDTO request) { + } + } + + String actualJson = generateSchemaJson(ToolWithNested.class, "execute", NestedDTO.class); + + String expectedJson = """ + { + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "The URI" + }, + "line": { + "type": "integer", + "description": "The line number" + } + }, + "required": [ + "uri", + "line" + ] + } + }, + "required": [ + "request" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + @Test + void testNestedObject_separateRequiredArrays_bugFix() throws Exception { + // CRITICAL: Verifies the bug fix where nested required arrays + // were incorrectly sharing the same list as the root required array + class NestedDTO { + @ToolArg(description = "Required field 1") + public String getField1() { + return null; + } + + @ToolArg(description = "Required field 2") + public String getField2() { + return null; + } + } + + class ToolWithNested implements McpTool { + @Tool(description = "Test") + public void execute(@ToolArg(name = "request", description = "Request") NestedDTO request) { + } + } + + String actualJson = generateSchemaJson(ToolWithNested.class, "execute", NestedDTO.class); + + // The bug was: nested required contained ["request", "field1", "field2"] + // The fix ensures: root required = ["request"], nested required = ["field1", "field2"] + String expectedJson = """ + { + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + "field1": { + "type": "string", + "description": "Required field 1" + }, + "field2": { + "type": "string", + "description": "Required field 2" + } + }, + "required": [ + "field1", + "field2" + ] + } + }, + "required": [ + "request" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + @Test + void testMultipleParameters() throws Exception { + class MultiParamTool implements McpTool { + @Tool(description = "Test") + public void execute( + @ToolArg(name = "param1", description = "First") String p1, + @ToolArg(name = "param2", description = "Second") int p2) { + } + } + + String actualJson = generateSchemaJson(MultiParamTool.class, "execute", String.class, int.class); + + String expectedJson = """ + { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First" + }, + "param2": { + "type": "integer", + "description": "Second" + } + }, + "required": [ + "param1", + "param2" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + @Test + void testTypeMapping_allPrimitives() throws Exception { + class TypeTool implements McpTool { + @Tool(description = "Test") + public void execute( + @ToolArg(name = "str", description = "String param") String s, + @ToolArg(name = "i", description = "Int param") int i, + @ToolArg(name = "l", description = "Long param") long l, + @ToolArg(name = "d", description = "Double param") double d, + @ToolArg(name = "f", description = "Float param") float f, + @ToolArg(name = "b", description = "Boolean param") boolean b) { + } + } + + String actualJson = generateSchemaJson(TypeTool.class, "execute", + String.class, int.class, long.class, double.class, float.class, boolean.class); + + String expectedJson = """ + { + "type": "object", + "properties": { + "str": { + "type": "string", + "description": "String param" + }, + "i": { + "type": "integer", + "description": "Int param" + }, + "l": { + "type": "integer", + "description": "Long param" + }, + "d": { + "type": "number", + "description": "Double param" + }, + "f": { + "type": "number", + "description": "Float param" + }, + "b": { + "type": "boolean", + "description": "Boolean param" + } + }, + "required": [ + "str", + "i", + "l", + "d", + "f", + "b" + ] + } + """; + + assertJsonEquals(expectedJson, actualJson); + } + + // Helper methods + + private String generateSchemaJson(Class toolClass, String methodName, Class... paramTypes) + throws Exception { + Method method = toolClass.getMethod(methodName, paramTypes); + Map schema = generateInputSchema(method); + return gson.toJson(schema); + } + + @SuppressWarnings("unchecked") + private Map generateInputSchema(Method method) throws Exception { + Method generateMethod = McpToolRegistry.class.getDeclaredMethod("generateInputSchema", Method.class); + generateMethod.setAccessible(true); + return (Map) generateMethod.invoke(registry, method); + } + + @SuppressWarnings("unchecked") + private void assertJsonEquals(String expectedJson, String actualJson) { + // Parse both JSON strings + Object expected = gson.fromJson(expectedJson, Object.class); + Object actual = gson.fromJson(actualJson, Object.class); + + // Normalize "required" arrays (sort them) since reflection order is not guaranteed + normalizeRequiredArrays(expected); + normalizeRequiredArrays(actual); + + // Compare objects directly (Map.equals() ignores key order) + assertEquals(expected, actual, + "\nExpected JSON:\n" + gson.toJson(expected) + "\n\nActual JSON:\n" + gson.toJson(actual)); + } + + @SuppressWarnings("unchecked") + private void normalizeRequiredArrays(Object obj) { + if (obj instanceof Map) { + Map map = (Map) obj; + // Sort "required" array if present + if (map.containsKey("required") && map.get("required") instanceof List) { + List required = (List) map.get("required"); + required.sort(String::compareTo); + } + // Recursively normalize nested objects + for (Object value : map.values()) { + normalizeRequiredArrays(value); + } + } else if (obj instanceof List) { + for (Object item : (List) obj) { + normalizeRequiredArrays(item); + } + } + } +} diff --git a/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpMethodArgsDeserializationTest.java b/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpMethodArgsDeserializationTest.java new file mode 100644 index 000000000..8bc24d870 --- /dev/null +++ b/lsp4j-mcp/src/test/java/com/redhat/lsp4j/mcp/server/McpMethodArgsDeserializationTest.java @@ -0,0 +1,274 @@ +/******************************************************************************* + * Copyright (c) 2026 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.lsp4j.mcp.server; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.redhat.lsp4j.mcp.annotations.Tool; +import com.redhat.lsp4j.mcp.annotations.ToolArg; +import com.redhat.lsp4j.mcp.cache.McpCache; +import com.redhat.lsp4j.mcp.tools.McpTool; +import com.redhat.lsp4j.mcp.tools.Position; +import com.redhat.lsp4j.mcp.tools.TextDocumentIdentifier; + +import io.modelcontextprotocol.spec.McpSchema; + +/** + * Tests for extractMethodArgs - validates that MCP arguments (Map) + * are correctly deserialized into Java method parameters via Gson. + */ +class McpMethodArgsDeserializationTest { + + private McpToolRegistry registry; + private McpCache cache; + + @BeforeEach + void setUp() { + registry = new McpToolRegistry(); + cache = new McpCache(); + registry.register(McpCache.class, cache); + } + + @Test + void testSimpleStringParameter() { + class SimpleTool implements McpTool { + String receivedValue = null; + + @Tool(description = "Test") + public void execute(@ToolArg(name = "input", description = "Input") String input) { + this.receivedValue = input; + } + } + + SimpleTool tool = new SimpleTool(); + registry.scanAndRegister(tool); + + // Simulate MCP client sending arguments + Map args = new HashMap<>(); + args.put("input", "hello world"); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + assertEquals("hello world", tool.receivedValue); + } + + @Test + void testTextDocumentIdentifier_nestedMap() { + class TestTool implements McpTool { + TextDocumentIdentifier receivedDoc = null; + + @Tool(description = "Test") + public void execute( + @ToolArg(name = "textDocument", description = "Doc") TextDocumentIdentifier textDocument) { + this.receivedDoc = textDocument; + } + } + + TestTool tool = new TestTool(); + registry.scanAndRegister(tool); + + // Simulate MCP client sending: {textDocument={uri=file:///path/to/file}} + Map textDocMap = new HashMap<>(); + textDocMap.put("uri", "file:///C:/Users/Test/file.html"); + + Map args = new HashMap<>(); + args.put("textDocument", textDocMap); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + // CRITICAL TEST: Verify TextDocumentIdentifier.uri was correctly deserialized + assertNotNull(tool.receivedDoc, "TextDocumentIdentifier should not be null"); + assertEquals("file:///C:/Users/Test/file.html", tool.receivedDoc.getUri(), + "URI should be correctly deserialized from nested map"); + } + + static class NestedDTO { + private Position position; + + @ToolArg(description = "Position in file") + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + } + + @Test + void testNestedObject_withPosition() { + class TestTool implements McpTool { + NestedDTO receivedData = null; + + @Tool(description = "Test") + public void execute(@ToolArg(name = "data", description = "Data") NestedDTO data) { + this.receivedData = data; + } + } + + TestTool tool = new TestTool(); + registry.scanAndRegister(tool); + + // Nested structure: {data={position={line=5, character=10}}} + Map posMap = new HashMap<>(); + posMap.put("line", 5); + posMap.put("character", 10); + + Map dataMap = new HashMap<>(); + dataMap.put("position", posMap); + + Map args = new HashMap<>(); + args.put("data", dataMap); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + assertNotNull(tool.receivedData); + assertNotNull(tool.receivedData.getPosition()); + assertEquals(5, tool.receivedData.getPosition().getLine()); + assertEquals(10, tool.receivedData.getPosition().getCharacter()); + } + + @Test + void testParameterWithoutName_arg0() { + class TestTool implements McpTool { + String receivedValue = null; + + @Tool(description = "Test") + public void execute(String unnamed) { + this.receivedValue = unnamed; + } + } + + TestTool tool = new TestTool(); + registry.scanAndRegister(tool); + + // Without @ToolArg(name=...), parameter is named "arg0" + Map args = new HashMap<>(); + args.put("arg0", "test value"); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + assertEquals("test value", tool.receivedValue); + } + + @Test + void testMultipleParameters() { + class TestTool implements McpTool { + String receivedStr = null; + int receivedInt = 0; + TextDocumentIdentifier receivedDoc = null; + + @Tool(description = "Test") + public void execute( + @ToolArg(name = "str", description = "") String str, + @ToolArg(name = "num", description = "") int num, + @ToolArg(name = "doc", description = "") TextDocumentIdentifier doc) { + this.receivedStr = str; + this.receivedInt = num; + this.receivedDoc = doc; + } + } + + TestTool tool = new TestTool(); + registry.scanAndRegister(tool); + + Map docMap = new HashMap<>(); + docMap.put("uri", "file:///multi.html"); + + Map args = new HashMap<>(); + args.put("str", "hello"); + args.put("num", 42); + args.put("doc", docMap); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + assertEquals("hello", tool.receivedStr); + assertEquals(42, tool.receivedInt); + assertNotNull(tool.receivedDoc); + assertEquals("file:///multi.html", tool.receivedDoc.getUri()); + } + + @Test + void testPrimitiveTypes() { + class TestTool implements McpTool { + int receivedInt = 0; + double receivedDouble = 0.0; + boolean receivedBool = false; + + @Tool(description = "Test") + public void execute( + @ToolArg(name = "i", description = "") int i, + @ToolArg(name = "d", description = "") double d, + @ToolArg(name = "b", description = "") boolean b) { + this.receivedInt = i; + this.receivedDouble = d; + this.receivedBool = b; + } + } + + TestTool tool = new TestTool(); + registry.scanAndRegister(tool); + + Map args = new HashMap<>(); + args.put("i", 123); + args.put("d", 3.14); + args.put("b", true); + + McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder() + .name("execute") + .arguments(args) + .build(); + + LspMcpServer.McpToolRegistration toolReg = registry.getToolRegistrations().get(0); + toolReg.getHandler().execute(null, request); + + assertEquals(123, tool.receivedInt); + assertEquals(3.14, tool.receivedDouble, 0.001); + assertTrue(tool.receivedBool); + } +} diff --git a/qute.ls/com.redhat.qute.ls/pom.xml b/qute.ls/com.redhat.qute.ls/pom.xml index ec9e074ca..1dbf41cc0 100644 --- a/qute.ls/com.redhat.qute.ls/pom.xml +++ b/qute.ls/com.redhat.qute.ls/pom.xml @@ -41,7 +41,8 @@ 0.24.0 3.30.1 5.6.1 - 2.18.2 + 2.0.0-M2 + 2.3.17.Final @@ -185,12 +186,55 @@ ${junit.version} test - + - com.fasterxml.jackson.dataformat + tools.jackson.dataformat jackson-dataformat-yaml - ${jackson-dataformat-yaml.version} - + 3.0.3 + + + + com.redhat.lsp4j + lsp4j-mcp + 0.1.0-SNAPSHOT + + + + io.modelcontextprotocol.sdk + mcp + ${mcp.version} + + + com.fasterxml.jackson.core + * + + + com.fasterxml.jackson.datatype + * + + + + + io.modelcontextprotocol.sdk + mcp-json-jackson3 + ${mcp.version} + + + com.fasterxml.jackson.core + * + + + com.fasterxml.jackson.datatype + * + + + + + + io.undertow + undertow-servlet + ${undertow.version} + diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/QuteServerLauncher.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/QuteServerLauncher.java index 1291ff66b..dfca9d425 100644 --- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/QuteServerLauncher.java +++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/QuteServerLauncher.java @@ -1,92 +1,129 @@ -/******************************************************************************* -* Copyright (c) 2021 Red Hat Inc. and others. -* All rights reserved. This program and the accompanying materials -* which accompanies this distribution, and is available at -* http://www.eclipse.org/legal/epl-v20.html -* -* SPDX-License-Identifier: EPL-2.0 -* -* Contributors: -* Red Hat Inc. - initial API and implementation -*******************************************************************************/ -package com.redhat.qute.ls; - -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Function; - -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.jsonrpc.MessageConsumer; -import org.eclipse.lsp4j.launch.LSPLauncher.Builder; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageServer; - -import com.redhat.qute.ls.api.QuteLanguageClientAPI; -import com.redhat.qute.ls.commons.ParentProcessWatcher; - -/** - * Qute server launcher - * - */ -public class QuteServerLauncher { - - /** - * Main entry point for the server. System properties may influence the - * behavior: - *
    - * watchParentProcess: if defined and value is false then do not watch - * for the parent process otherwise if parent process is dead then stop this - * server. - *
- *
    - * runAsync: if defined and value is true then received message are - * processed in a separate thread than the LSP4J thread. - *
- * - * @param args - */ - public static void main(String[] args) { - QuteLanguageServer server = new QuteLanguageServer(); - Function wrapper; - wrapper = it -> it; - if ("true".equals(System.getProperty("runAsync"))) { - wrapper = it -> msg -> CompletableFuture.runAsync(() -> it.consume(msg)); - } - if (!"false".equals(System.getProperty("watchParentProcess"))) { - wrapper = new ParentProcessWatcher(server, wrapper); - } - Launcher launcher = createServerLauncher(server, System.in, System.out, - Executors.newCachedThreadPool(), wrapper); - - server.setClient(launcher.getRemoteProxy()); - launcher.startListening(); - } - - /** - * Create a new Launcher for a language server and an input and output stream. - * Threads are started with the given executor service. The wrapper function is - * applied to the incoming and outgoing message streams so additional message - * handling such as validation and tracing can be included. - * - * @param server - the server that receives method calls from the - * remote client - * @param in - input stream to listen for incoming messages - * @param out - output stream to send outgoing messages - * @param executorService - the executor service used to start threads - * @param wrapper - a function for plugging in additional message - * consumers - */ - public static Launcher createServerLauncher(LanguageServer server, InputStream in, OutputStream out, - ExecutorService executorService, Function wrapper) { - return new Builder().setLocalService(server).setRemoteInterface(QuteLanguageClientAPI.class) // Set - // client - // as - // Quarkus - // language - // client - .setInput(in).setOutput(out).setExecutorService(executorService).wrapMessages(wrapper).create(); - } -} +/******************************************************************************* +* Copyright (c) 2021 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package com.redhat.qute.ls; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.jsonrpc.MessageConsumer; +import org.eclipse.lsp4j.launch.LSPLauncher.Builder; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageServer; + +import com.redhat.lsp4j.mcp.cache.McpCache; +import com.redhat.lsp4j.mcp.server.LspMcpServer; +import com.redhat.lsp4j.mcp.server.McpLanguageClientWrapper; +import com.redhat.lsp4j.mcp.server.McpLanguageServerWrapper; +import com.redhat.qute.ls.api.QuteLanguageClientAPI; +import com.redhat.qute.ls.api.QuteLanguageServerAPI; +import com.redhat.qute.ls.commons.ParentProcessWatcher; + +/** + * Qute server launcher + * + */ +public class QuteServerLauncher { + + /** + * Main entry point for the server. System properties may influence the + * behavior: + *
    + * watchParentProcess: if defined and value is false then do not watch + * for the parent process otherwise if parent process is dead then stop this + * server. + *
+ *
    + * runAsync: if defined and value is true then received message are + * processed in a separate thread than the LSP4J thread. + *
+ * + * @param args + */ + public static void main(String[] args) { + QuteLanguageServer server = new QuteLanguageServer(); + + // Create MCP cache shared between wrappers + McpCache mcpCache = new McpCache(); + + // Wrap the Language Server to intercept didOpen/didClose + QuteLanguageServerAPI wrappedServer = McpLanguageServerWrapper.wrap( + QuteLanguageServerAPI.class, server, mcpCache); + + Function wrapper; + wrapper = it -> it; + if ("true".equals(System.getProperty("runAsync"))) { + wrapper = it -> msg -> CompletableFuture.runAsync(() -> it.consume(msg)); + } + if (!"false".equals(System.getProperty("watchParentProcess"))) { + wrapper = new ParentProcessWatcher(wrappedServer, wrapper); + } + Launcher launcher = createServerLauncher(wrappedServer, System.in, System.out, + Executors.newCachedThreadPool(), wrapper); + + // Wrap the client to intercept publishDiagnostics + QuteLanguageClientAPI client = (QuteLanguageClientAPI) launcher.getRemoteProxy(); + QuteLanguageClientAPI wrappedClient = McpLanguageClientWrapper.wrap( + QuteLanguageClientAPI.class, client, mcpCache); + + server.setClient(wrappedClient); + + // Start MCP server with annotation-based tools + LspMcpServer mcpServer = LspMcpServer.builder() + .serverInfo("qute-ls", "0.25.0") + .port(9339) + .registerDependency(org.eclipse.lsp4j.services.LanguageServer.class, wrappedServer) + .registerDependency(com.redhat.lsp4j.mcp.cache.McpCache.class, mcpCache) + .build(); // Auto-discovers tools via SPI + + // Start MCP server in background thread + new Thread(() -> { + try { + Thread.sleep(2000); // Wait for LS to be fully initialized + mcpServer.start(); + } catch (Exception e) { + e.printStackTrace(); + } + }, "MCP-Server-Starter").start(); + + launcher.startListening(); + } + + /** + * Create a new Launcher for a language server and an input and output stream. + * Threads are started with the given executor service. The wrapper function is + * applied to the incoming and outgoing message streams so additional message + * handling such as validation and tracing can be included. + * + * @param server - the server that receives method calls from the + * remote client + * @param in - input stream to listen for incoming messages + * @param out - output stream to send outgoing messages + * @param executorService - the executor service used to start threads + * @param wrapper - a function for plugging in additional message + * consumers + */ + public static Launcher createServerLauncher(LanguageServer server, InputStream in, OutputStream out, + ExecutorService executorService, Function wrapper) { + return new Builder().setLocalService(server).setRemoteInterface(QuteLanguageClientAPI.class) // Set + // client + // as + // Quarkus + // language + // client + .setInput(in).setOutput(out).setExecutorService(executorService).wrapMessages(wrapper).create(); + } +} diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/api/QuteLanguageServerAPI.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/api/QuteLanguageServerAPI.java index 4e365f74e..526c43379 100644 --- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/api/QuteLanguageServerAPI.java +++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/ls/api/QuteLanguageServerAPI.java @@ -12,10 +12,10 @@ package com.redhat.qute.ls.api; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; -import org.eclipse.lsp4j.services.LanguageServer; import com.redhat.qute.commons.ProjectInfo; import com.redhat.qute.commons.datamodel.JavaDataModelChangeEvent; +import com.redhat.qute.ls.commons.ParentProcessWatcher.ProcessLanguageServer; /** * Qute language server API. @@ -23,7 +23,9 @@ * @author Angelo ZERR * */ -public interface QuteLanguageServerAPI extends LanguageServer { +public interface QuteLanguageServerAPI extends ProcessLanguageServer, QuteProjectInfoProvider, QuteJavaTypesProvider, + QuteResolvedJavaTypeProvider, QuteJavaDefinitionProvider, QuteDataModelProjectProvider, + QuteBinaryTemplateProvider, QuteJavadocProvider, QuteTemplateProvider { /** * Notification for Qute data model changed which occurs when: @@ -38,7 +40,7 @@ public interface QuteLanguageServerAPI extends LanguageServer { */ @JsonNotification("qute/dataModelChanged") void dataModelChanged(JavaDataModelChangeEvent event); - + /** * Notification received when a Qute project is added in the workspace. * diff --git a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/data/yaml/YamlDataLoader.java b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/data/yaml/YamlDataLoader.java index 28b2fa63a..0f3b10d00 100644 --- a/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/data/yaml/YamlDataLoader.java +++ b/qute.ls/com.redhat.qute.ls/src/main/java/com/redhat/qute/project/extensions/roq/data/yaml/YamlDataLoader.java @@ -19,10 +19,10 @@ import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.yaml.YAMLMapper; import com.redhat.qute.commons.JavaFieldInfo; import com.redhat.qute.commons.ResolvedJavaTypeInfo; import com.redhat.qute.project.extensions.roq.data.DataLoader; @@ -127,7 +127,7 @@ private static List extractFieldsFromObject(ObjectNode objectNode List fields = new ArrayList<>(); // Iterate over all entries in the YAML map - objectNode.fields().forEachRemaining(entry -> { + objectNode.properties().forEach(entry -> { String fieldName = entry.getKey(); // YAML key becomes field name JsonNode value = entry.getValue(); // YAML value determines type