diff --git a/settings.gradle.kts b/settings.gradle.kts index b1682ee7..7bf24b93 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,3 +47,6 @@ module("html2png", "./tutorials/html2png") module("serve-from-directory", "./tutorials/serve-from-directory") module("js-java", "./tutorials/js-java") module("crx-extensions", "./tutorials/crx-extensions") +// Automation with MCP +module("mcp-devtools", "./tutorials/ai/mcp-devtools") +module("mcp-extension", "./tutorials/ai/mcp-extension") diff --git a/tutorials/ai/mcp-devtools/README.md b/tutorials/ai/mcp-devtools/README.md new file mode 100644 index 00000000..469fb860 --- /dev/null +++ b/tutorials/ai/mcp-devtools/README.md @@ -0,0 +1,98 @@ +# DevTools Protocol ↔ Playwright MCP ↔ LLM + +Automates JxBrowser using the [Playwright MCP server][playwright-mcp], +the Chrome DevTools Protocol, and an MCP host like [Cursor][cursor] or +[Claude Desktop][claude]. + +## Prerequisites + +- Java 17+ +- Node.js 20+ +- A valid [JxBrowser license][licensing] +- An MCP host, such as [Cursor][cursor] or [Claude Desktop][claude]. + +## Set up MCP server + +### Cursor + +1. Open Cursor. +2. Go to **Settings...** → **Cursor Settings** → **Tools & MCP**. +3. In the **Installed MCP Servers** tab, click **New MCP Server**. +4. Add the following JSON to your config: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--cdp-endpoint", + "http://localhost:9222" + ] + } + } +} +``` + +http://localhost:9222 must match the debugging URL used by JxBrowser. + +5. Save the config and ensure the **playwright** MCP server is enabled. + +### Claude Desktop + +1. Open Claude Desktop. +2. Go to **Settings...** → **Claude Desktop Settings** → **Developer**. +3. Click the **Edit Config** button to open the configuration file. +4. Add the following JSON to your config: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--cdp-endpoint", + "http://localhost:9222" + ] + } + } +} +``` + +http://localhost:9222 must match the debugging URL used by JxBrowser. + +5. Save the configuration file and restart Claude Desktop. + +## Run the application + +Clone this repository: + +```bash +git clone https://github.com/TeamDev-IP/JxBrowser-Examples +``` + +From the project root, run the app with your license key: + +```bash +./gradlew :mcp-devtools:run -Djxbrowser.license.key="" -Ddebugging.url=http://localhost:9222 +``` + +`-Ddebugging.url` must match the value in the MCP config. If omitted, it +defaults to http://localhost:9222. + +This launches a Java window with JxBrowser and enables remote debugging. + +## Start automating + +Open the AI chat in Cursor or Claude Desktop and try commands like: +> "Open Google and search for TeamDev. Return the company’s phone number." + +The MCP host will send requests to the Playwright MCP server, which controls +JxBrowser via the DevTools Protocol. + +[playwright-mcp]: https://github.com/microsoft/playwright-mcp +[cursor]: https://cursor.com/ +[claude]: https://claude.ai/download +[licensing]: https://teamdev.com/jxbrowser/docs/guides/introduction/licensing/ diff --git a/tutorials/ai/mcp-devtools/build.gradle.kts b/tutorials/ai/mcp-devtools/build.gradle.kts new file mode 100644 index 00000000..982d2ef0 --- /dev/null +++ b/tutorials/ai/mcp-devtools/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +plugins { + application +} + +application { + mainClass = "com.teamdev.jxbrowser.examples.DevToolsMCP" +} diff --git a/tutorials/ai/mcp-devtools/src/main/java/com/teamdev/jxbrowser/examples/DevToolsMCP.java b/tutorials/ai/mcp-devtools/src/main/java/com/teamdev/jxbrowser/examples/DevToolsMCP.java new file mode 100644 index 00000000..b6f4e4d4 --- /dev/null +++ b/tutorials/ai/mcp-devtools/src/main/java/com/teamdev/jxbrowser/examples/DevToolsMCP.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.teamdev.jxbrowser.examples; + +import com.teamdev.jxbrowser.browser.Browser; +import com.teamdev.jxbrowser.engine.Engine; +import com.teamdev.jxbrowser.engine.EngineOptions; +import com.teamdev.jxbrowser.view.swing.BrowserView; + +import javax.swing.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.net.URI; +import java.net.URISyntaxException; + +import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED; +import static javax.swing.SwingConstants.CENTER; +import static javax.swing.SwingUtilities.invokeLater; +import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE; + +/** + * An example app that demonstrates how to automate JxBrowser using + * the Chrome DevTools Protocol and a Playwright MCP server. + * + *

This example: + *

    + *
  1. Starts a Chromium engine with remote debugging enabled.
  2. + *
  3. Opens a JFrame with a BrowserView inside.
  4. + *
  5. Loads a web page and connects to the Playwright MCP server via the DevTools Protocol.
  6. + *
+ */ +public final class DevToolsMCP { + + /** + * The remote debugging URL used by default. + */ + private static final String DEBUGGING_URL = "http://localhost:9222"; + + /** + * The initial URL to load in the browser. + */ + private static final String START_URL = "https://teamdev.com"; + + public static void main(String[] args) { + var debuggingUrl = System.getProperty("debugging.url", DEBUGGING_URL); + var debuggingPort = extractPort(debuggingUrl); + var engine = Engine.newInstance( + EngineOptions.newBuilder(HARDWARE_ACCELERATED) + .addSwitch("--remote-allow-origins=" + debuggingUrl) + .remoteDebuggingPort(debuggingPort) + .build()); + var browser = engine.newBrowser(); + + showUI(browser); + browser.navigation().loadUrl(START_URL); + } + + private static int extractPort(String url) { + try { + URI uri = new URI(url); + int port = uri.getPort(); + return port > 0 ? port : 9222; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid debugging URL: " + url, e); + } + } + + private static void showUI(Browser browser) { + invokeLater(() -> { + var frame = new JFrame("JxBrowser DevTools MCP"); + frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + frame.setSize(1280, 900); + frame.setLocationRelativeTo(null); + frame.add(BrowserView.newInstance(browser), CENTER); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + browser.engine().close(); + } + }); + frame.setVisible(true); + }); + } +} diff --git a/tutorials/ai/mcp-extension/README.md b/tutorials/ai/mcp-extension/README.md new file mode 100644 index 00000000..4e766919 --- /dev/null +++ b/tutorials/ai/mcp-extension/README.md @@ -0,0 +1,62 @@ +# Chrome Extension ↔ Browser MCP ↔ LLM + +Automates JxBrowser using the [Browser MCP][browsermcp] Chrome extension +and [Cursor][cursor]. + +## Prerequisites + +- Java 17+ +- Node.js 20+ +- A valid [JxBrowser license][licensing] +- [Cursor][cursor] — a code editor that supports MCP + +## Set up MCP server + +1. Open Cursor. +2. Go to **Settings...** → **Cursor Settings** → **Tools & MCP**. +3. In the **Installed MCP Servers** tab, click **New MCP Server**. +4. Add the following JSON to your config: + +```json +{ + "mcpServers": { + "browsermcp": { + "command": "npx", + "args": [ + "@browsermcp/mcp" + ] + } + } +} +``` + +5. Save the config and ensure the **browsermcp** MCP server is enabled. + +## Run the application + +Clone this repository: + +```bash +git clone https://github.com/TeamDev-IP/JxBrowser-Examples +``` + +From the project root, run the app with your license key: + +```bash +./gradlew :mcp-extension:run -Djxbrowser.license.key="" +``` + +This launches a Java window with JxBrowser and loads the Browser MCP Chrome +extension. + +## Start automating + +Open the AI chat in Cursor and try commands like: +> "Open Google and search for TeamDev. Return the company’s phone number." + +Cursor will send requests to the Browser MCP server, which controls JxBrowser +via the installed Browser MCP Chrome extension. + +[browsermcp]: https://browsermcp.io/ +[cursor]: https://cursor.com/ +[licensing]: https://teamdev.com/jxbrowser/docs/guides/introduction/licensing/ diff --git a/tutorials/ai/mcp-extension/build.gradle.kts b/tutorials/ai/mcp-extension/build.gradle.kts new file mode 100644 index 00000000..da1ae3c5 --- /dev/null +++ b/tutorials/ai/mcp-extension/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +plugins { + application +} + +application { + mainClass = "com.teamdev.jxbrowser.examples.ExtensionMCP" +} diff --git a/tutorials/ai/mcp-extension/src/main/java/com/teamdev/jxbrowser/examples/ExtensionMCP.java b/tutorials/ai/mcp-extension/src/main/java/com/teamdev/jxbrowser/examples/ExtensionMCP.java new file mode 100644 index 00000000..072b1b5a --- /dev/null +++ b/tutorials/ai/mcp-extension/src/main/java/com/teamdev/jxbrowser/examples/ExtensionMCP.java @@ -0,0 +1,141 @@ +/* + * Copyright 2025, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.teamdev.jxbrowser.examples; + +import com.teamdev.jxbrowser.browser.Browser; +import com.teamdev.jxbrowser.browser.callback.OpenExtensionActionPopupCallback; +import com.teamdev.jxbrowser.engine.Engine; +import com.teamdev.jxbrowser.engine.EngineOptions; +import com.teamdev.jxbrowser.extensions.Extension; +import com.teamdev.jxbrowser.extensions.ExtensionAction; +import com.teamdev.jxbrowser.navigation.event.FrameDocumentLoadFinished; +import com.teamdev.jxbrowser.view.swing.BrowserView; + +import javax.swing.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED; +import static javax.swing.SwingConstants.CENTER; +import static javax.swing.SwingUtilities.invokeLater; +import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE; + +/** + * An example app that demonstrates how to automate JxBrowser using + * the Browser MCP Chrome extension and a local MCP server. + * + *

This example: + *

    + *
  1. Installs the Browser MCP Chrome extension in JxBrowser.
  2. + *
  3. Opens a JFrame with a BrowserView inside.
  4. + *
  5. Loads a web page and connects to the Browser MCP server via the extension popup.
  6. + *
+ */ +public final class ExtensionMCP { + + /** + * Path to the extension CRX file in the resources folder. + */ + private static final String EXTENSION_FILE = "browser-mcp.crx"; + + /** + * The initial URL to load in the browser. + */ + private static final String START_URL = "https://teamdev.com"; + + public static void main(String[] args) { + var engine = Engine.newInstance( + EngineOptions.newBuilder(HARDWARE_ACCELERATED) + .userDataDir(Paths.get("userData")) + .build()); + var browser = engine.newBrowser(); + var extension = installExtension(browser); + + showUI(browser); + + browser.navigation().loadUrlAndWait(START_URL); + extension.action(browser).ifPresent(ExtensionAction::click); + } + + private static Extension installExtension(Browser browser) { + var profile = browser.profile(); + var extensions = profile.extensions(); + + Path extensionPath = getExtensionPath(EXTENSION_FILE); + Extension extension = extensions.install(extensionPath); + + browser.set(OpenExtensionActionPopupCallback.class, (params, tell) -> { + var popupBrowser = params.popupBrowser(); + popupBrowser.navigation().on(FrameDocumentLoadFinished.class, event -> { + // Auto-click the "Connect" button. + event.frame().executeJavaScript( + "setTimeout(() => document.querySelector('button.w-full')?.click(), 1000);" + ); + }); + tell.proceed(); + }); + + return extension; + } + + private static void showUI(Browser browser) { + invokeLater(() -> { + var frame = new JFrame("JxBrowser Extension MCP"); + frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + frame.setSize(1280, 900); + frame.setLocationRelativeTo(null); + frame.add(BrowserView.newInstance(browser), CENTER); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + browser.engine().close(); + } + }); + frame.setVisible(true); + }); + } + + /** + * Extracts the extension CRX file to a temporary location on disk, and returns + * the absolute file system path for installation. + * + *

We intentionally avoid using {@code getResource().toURI()} to locate the extension + * file directly, because it doesn't work when the application is packaged into a JAR. + */ + private static Path getExtensionPath(String extensionFileName) { + var resource = ExtensionMCP.class.getClassLoader().getResourceAsStream(extensionFileName); + if (resource == null) { + throw new IllegalStateException("Missing Chrome extension CRX file: " + extensionFileName); + } + try { + Path tempFile = Files.createTempFile("extension-", ".crx"); + tempFile.toFile().deleteOnExit(); + Files.copy(resource, tempFile, StandardCopyOption.REPLACE_EXISTING); + return tempFile; + } catch (Exception e) { + throw new IllegalStateException("Failed to extract Chrome extension CRX file from resources", e); + } + } +} diff --git a/tutorials/ai/mcp-extension/src/main/resources/browser-mcp.crx b/tutorials/ai/mcp-extension/src/main/resources/browser-mcp.crx new file mode 100644 index 00000000..2e31f4df Binary files /dev/null and b/tutorials/ai/mcp-extension/src/main/resources/browser-mcp.crx differ