In this tutorial, we’ll demonstrate how to use the jDeploy desktop lib to add deep-linking support to a Swing desktop application. We’ll configure jDeploy using the jDeploy MCP server with an AI coding agent.

Tip
This tutorial is also available in variants that show how to configure jDeploy by editing package.json directly or using the GUI application.

Background

This tutorial is a follow-up to The Desktop Companion App Pattern, which introduced the concept of using custom URL schemes to enable communication between web applications and desktop apps.

In that article, we demonstrated basic URL scheme handling, but there was a limitation: on Windows and Linux, opening a custom URL would launch a new process each time, with the URL passed as a command-line argument. This wasn’t ideal for most use cases, as it required managing multiple instances or manually implementing single-instance behavior.

jDeploy 6.0 introduces singleton mode, which ensures only one instance of your application runs at a time. When a custom URL is opened, the existing instance receives the URL via a callback — the same behavior macOS provides natively. This makes the companion app pattern much more practical, enabling simple inter-process communication between your web app and desktop app without spawning duplicate processes.

What is Deep Linking?

Deep linking allows users to navigate directly to specific content or functionality within an application using a URL. This is particularly useful for desktop applications, as it enables users to access specific features or data without having to navigate through the application’s interface.

Some examples of deep linking include:

  • A user clicking on a link in an email that opens a specific page within a desktop application.

  • A user sharing a link to a specific feature or content within a desktop application on social media.

  • A background service or web dashboard opening a specific view in the desktop app via a custom URL.

Prerequisites

This tutorial assumes you have:

  • A basic Swing application

  • jDeploy 6.0 or later installed with MCP integration enabled

  • An AI coding tool that supports MCP (Claude Code, VS Code Copilot, Cursor, etc.)

  • Familiarity with Maven or Gradle for dependency management

Overview

To add deep linking to your Swing application, you need to:

  1. Add the jdeploy-desktop-lib-swing dependency to your project

  2. Use your AI coding agent to configure singleton mode and a custom URL scheme

  3. Implement a JDeployOpenHandler to receive and handle deep link events

Note
This tutorial assumes you have the jDeploy MCP server connected to your AI tool. See the jDeploy 6.0 Release Notes for setup instructions.

Add the Dependency

Add the jdeploy-desktop-lib-swing dependency to your project.

Gradle:

dependencies {
    implementation 'ca.weblite:jdeploy-desktop-lib-swing:1.0.2'
}

Maven:

<dependency>
    <groupId>ca.weblite</groupId>
    <artifactId>jdeploy-desktop-lib-swing</artifactId>
    <version>1.0.2</version>
</dependency>

Configure jDeploy with Your AI Agent

With the MCP integration active, your AI coding agent can configure jDeploy directly. Open your project directory in your AI tool and use natural language to request the configuration.

Example Prompts

Here are some example prompts you can use with your AI coding agent:

Basic deep linking setup:

Configure jDeploy for this project with singleton mode enabled and
a custom URL scheme called "myapp"

More detailed request:

Set up deep linking for my Swing app. I want:
- Singleton mode so only one instance runs at a time
- A custom URL scheme "myapp" so I can open the app with myapp:// links

Combined with other features:

Configure jDeploy with:
- Singleton mode enabled
- Custom URL scheme "myapp"
- A helper action in the system tray that opens myapp://settings

What the AI Agent Does

When you make this request, the AI agent uses jDeploy’s MCP server to:

  1. Read your current package.json configuration

  2. Add or update the singleton setting to true

  3. Add your URL scheme to the urlSchemes array

  4. Save the updated configuration

The resulting package.json will include:

{
  "jdeploy": {
    "singleton": true,
    "urlSchemes": ["myapp"]
  }
}

Implement the Open Handler

The JDeployOpenHandler interface defines three callback methods that your application implements to handle external events:

  • openFiles(List<File> files) — Called when files should be opened (e.g., double-click, drag-drop, or "Open With")

  • openURIs(List<URI> uris) — Called when custom URL schemes are activated (deep links)

  • appActivated() — Called when the application should be brought to the foreground

Register your handler early in your application’s initialization using JDeploySwingApp.setOpenHandler(). All callbacks are dispatched on the Swing Event Dispatch Thread (EDT), so you can safely update your UI directly.

import ca.weblite.jdeploy.app.JDeployOpenHandler;
import ca.weblite.jdeploy.app.swing.JDeploySwingApp;
import javax.swing.*;
import java.io.File;
import java.net.URI;
import java.util.List;

public class MyApp {
    private JFrame mainWindow;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new MyApp().init();
        });
    }

    private void init() {
        mainWindow = new JFrame("My App");
        mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainWindow.setSize(800, 600);

        // Register the deep link handler
        JDeploySwingApp.setOpenHandler(new JDeployOpenHandler() {
            @Override
            public void openFiles(List<File> files) {
                for (File file : files) {
                    openDocument(file);
                }
            }

            @Override
            public void openURIs(List<URI> uris) {
                for (URI uri : uris) {
                    handleDeepLink(uri);
                }
            }

            @Override
            public void appActivated() {
                // Bring window to front
                mainWindow.setState(JFrame.NORMAL);
                mainWindow.toFront();
                mainWindow.requestFocus();
            }
        });

        mainWindow.setVisible(true);
    }

    private void openDocument(File file) {
        System.out.println("Opening file: " + file.getAbsolutePath());
        // Load and display the file content
    }

    private void handleDeepLink(URI uri) {
        System.out.println("Deep link received: " + uri);
        // Route to the appropriate view based on the URI
    }
}

When a deep link is received, parse the URI to determine what action to take. The URI structure typically follows this pattern:

myapp://host/path?query=value#fragment

For example:

  • myapp://settings — Open the settings page

  • myapp://user/123 — Open user profile with ID 123

  • myapp://document?id=abc&mode=edit — Open document "abc" in edit mode

Here’s how to parse and route deep links:

private void handleDeepLink(URI uri) {
    String scheme = uri.getScheme();     // "myapp"
    String host = uri.getHost();         // "settings", "user", "document"
    String path = uri.getPath();         // "/123", "", etc.
    String query = uri.getQuery();       // "id=abc&mode=edit"

    switch (host) {
        case "settings":
            showSettingsPanel();
            break;
        case "user":
            if (path != null && path.length() > 1) {
                String userId = path.substring(1); // Remove leading "/"
                showUserProfile(userId);
            }
            break;
        case "document":
            Map<String, String> params = parseQueryString(query);
            String docId = params.get("id");
            String mode = params.getOrDefault("mode", "view");
            openDocument(docId, mode);
            break;
        default:
            System.out.println("Unknown deep link: " + uri);
    }
}

private Map<String, String> parseQueryString(String query) {
    Map<String, String> params = new HashMap<>();
    if (query != null && !query.isEmpty()) {
        for (String param : query.split("&")) {
            String[] keyValue = param.split("=", 2);
            if (keyValue.length == 2) {
                params.put(keyValue[0], keyValue[1]);
            }
        }
    }
    return params;
}

Complete Example

Here’s a complete example that demonstrates deep linking with a tabbed interface. Clicking a deep link switches to the corresponding tab.

import ca.weblite.jdeploy.app.JDeployOpenHandler;
import ca.weblite.jdeploy.app.swing.JDeploySwingApp;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.net.URI;
import java.util.List;

public class DeepLinkDemo {
    private JFrame frame;
    private JTabbedPane tabbedPane;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new DeepLinkDemo().init();
        });
    }

    private void init() {
        frame = new JFrame("Deep Link Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(600, 400);

        // Create tabbed pane with sample tabs
        tabbedPane = new JTabbedPane();
        tabbedPane.addTab("Home", createPanel("Welcome to the Home tab"));
        tabbedPane.addTab("Settings", createPanel("Application Settings"));
        tabbedPane.addTab("Profile", createPanel("User Profile"));

        frame.add(tabbedPane, BorderLayout.CENTER);

        // Add instructions
        JLabel instructions = new JLabel(
            "<html>Try these deep links in your browser:<br>" +
            "&bull; demo://home<br>" +
            "&bull; demo://settings<br>" +
            "&bull; demo://profile</html>"
        );
        instructions.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        frame.add(instructions, BorderLayout.SOUTH);

        // Register deep link handler
        JDeploySwingApp.setOpenHandler(new JDeployOpenHandler() {
            @Override
            public void openFiles(List<File> files) {
                // Handle file opens if needed
            }

            @Override
            public void openURIs(List<URI> uris) {
                for (URI uri : uris) {
                    navigateTo(uri.getHost());
                }
            }

            @Override
            public void appActivated() {
                frame.setState(JFrame.NORMAL);
                frame.toFront();
                frame.requestFocus();
            }
        });

        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    private void navigateTo(String destination) {
        int tabIndex = switch (destination) {
            case "home" -> 0;
            case "settings" -> 1;
            case "profile" -> 2;
            default -> -1;
        };

        if (tabIndex >= 0) {
            tabbedPane.setSelectedIndex(tabIndex);
        }
    }

    private JPanel createPanel(String text) {
        JPanel panel = new JPanel(new BorderLayout());
        JLabel label = new JLabel(text, SwingConstants.CENTER);
        label.setFont(label.getFont().deriveFont(18f));
        panel.add(label, BorderLayout.CENTER);
        return panel;
    }
}

To configure this example with your AI agent, use a prompt like:

Configure jDeploy for this project with:
- Java 17
- Title "Deep Link Demo"
- Singleton mode enabled
- URL scheme "demo"

How It Works

Singleton Mode

When singleton mode is enabled, only one instance of your GUI application runs at a time. If the user launches the app again — whether by double-clicking a file, clicking a custom URL, or running the launcher command — the existing window is activated and the file or URI is forwarded to it.

On Windows and Linux, jDeploy uses file-based IPC to communicate between the secondary launch and the primary instance. On macOS, the operating system handles single-instance behavior natively through the Desktop API.

Event Queueing

If deep link events arrive before your handler is registered, they are queued and delivered as soon as you call setOpenHandler(). This ensures you never miss events during application startup.

Thread Safety

All callbacks from JDeploySwingApp are dispatched on the Swing Event Dispatch Thread (EDT). You can safely update your UI components directly in the handler methods without wrapping them in SwingUtilities.invokeLater().

Summary

Adding deep linking to a Swing application with jDeploy involves:

  1. Add the dependency — ca.weblite:jdeploy-desktop-lib-swing:1.0.2

  2. Configure jDeploy — Ask your AI agent to enable singleton mode and register URL schemes

  3. Implement JDeployOpenHandler — Register handlers for files, URIs, and app activation

  4. Parse and route URIs — Extract host, path, and query parameters to navigate your app

Using the MCP integration, you can configure jDeploy entirely through natural language conversation with your AI coding agent, without manually editing configuration files or navigating the GUI.

See Also