Documentation · Visual editor · Quick start · Example plugin · Discord
Server Connect lets your Minecraft server add its own menu, buttons, panels and live information to Leaf Client. Your server sends JSON plugin messages on one channel, leafclient:connect, and Leaf Client draws them in its own style. Players who do not use Leaf see nothing and send nothing.
This SDK is the Java side. It builds the JSON for you, checks it against the client limits before anything is sent, batches it, and checks every click that comes back before your handler runs.
The shop tab of the example plugin, in game.
Note
Server Connect is in beta. Messages, fields and limits can still change between Leaf releases. Check the client's caps before you use a feature, and report problems in the Discord.
- Server menu. A full screen menu with your title, icon and up to 8 tabs. Players open it with their Server Connect key (J by default).
- Panels in 8 places. Menu tabs, beside the inventory, beside container screens such as chests, above the chat, on the HUD, in front of the player, at a block, and on an entity or player.
- Elements. Text, buttons, badges, icons, images from a resource pack or an https link, 3D player models, player heads, lists, progress bars, leaderboards, scroll areas, and rows, columns, grids and layers.
- Forms. Text fields, toggles, checkboxes, choices and sliders. Their values arrive with the click, already checked against the panel you sent.
- Button actions. Send the click to your server, run a command, open a link, copy text, suggest a chat message, open the menu or a panel, close, or set a waypoint. Commands, links and copied text always ask the player first.
- Live values and timers.
{v:key},{n:key}and{t:key}placeholders change shown text without sending the UI again. Conditions show, hide or enable elements on the client with no round trip. - Updates in place.
Patchchanges fields of a panel the player already has. - Toasts and waypoints. Leaf notifications with up to 2 buttons, and server waypoints.
- Mod locks.
Session.lockModsturns off chosen client mods on your server and shows the player why. - Checked before sending. A value that breaks a client limit throws
InvalidUiExceptionwith the field path, so the client never receives UI it would reject. - Checked clicks. Your handler runs only for clicks that pass the SDK checks: known panel, matching token, enabled button, cooldown, valid inputs and, on Bukkit, reach for world panels.
- Batching. Messages queued in the same tick go out in one frame, large messages are split into chunks, and sending stays within the client rate limits. Sessions are thread safe.
- Old and new servers. The core and Bukkit modules target Java 8 and run with the Gson 2.2.4 that Spigot 1.8.8 ships. Behind a proxy,
onReadyruns again after every backend switch.
Build menus and panels without code in the Server Connect editor. The preview runs Leaf Client's own layout engine in the browser, so panels are laid out the way the game lays them out. When the design is ready, press Get code and download the plugin code for Paper / Spigot, Velocity or BungeeCord, or copy the raw JSON.
Editing a menu tab. The structure is on the left, the selected button's properties on the right.
The same tab in the editor's whole screen preview.
The full guide is on the Leaf website: https://leafclient.com/docs/#server-connect. Every example there shows the SDK code and the raw JSON it sends.
| Module | Package | For | Java |
|---|---|---|---|
core |
com.leafclient.connect.sdk |
Builders, sessions, batching and click checks | 8+ |
bukkit |
com.leafclient.connect.sdk.bukkit |
Paper, Spigot, Bukkit and Purpur, 1.8.8 to 26.x | 8+ |
velocity |
com.leafclient.connect.sdk.velocity |
Velocity proxies | 17+ |
bungee |
com.leafclient.connect.sdk.bungee |
BungeeCord and Waterfall proxies | 8+ |
Each platform module includes core. Shade the one you need into your plugin jar. The SDK uses the Gson library your server already ships, and registers the leafclient:connect channel in code, so plugin.yml needs no channels entry.
The SDK is on JitPack. The examples add bukkit. For a proxy plugin, use velocity or bungee instead.
Gradle (build.gradle.kts), then build with ./gradlew shadowJar:
plugins {
java
id("com.gradleup.shadow") version "8.3.5"
}
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.LeafClientMC.ServerConnectSDK:bukkit:v0.1.1")
}Maven (pom.xml), then build with mvn package:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.LeafClientMC.ServerConnectSDK</groupId>
<artifactId>bukkit</artifactId>
<version>v0.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>package com.example.shop;
import com.leafclient.connect.sdk.Condition;
import com.leafclient.connect.sdk.Icon;
import com.leafclient.connect.sdk.Menu;
import com.leafclient.connect.sdk.Panel;
import com.leafclient.connect.sdk.Place;
import com.leafclient.connect.sdk.Session;
import com.leafclient.connect.sdk.Toast;
import com.leafclient.connect.sdk.Ui;
import com.leafclient.connect.sdk.bukkit.BukkitConnect;
import org.bukkit.plugin.java.JavaPlugin;
public final class ShopPlugin extends JavaPlugin {
private BukkitConnect connect;
@Override
public void onEnable() {
connect = BukkitConnect.enable(this);
connect.onReady(this::sendUi);
}
@Override
public void onDisable() {
connect.disable();
}
private void sendUi(Session s) {
s.send(Menu.create("Example Network")
.subtitle("{n:coins} coins")
.icon(Icon.item("minecraft:emerald"))
.add(Menu.tab("shop", "Shop").icon(Icon.leaf("shop"))));
s.setValue("coins", 1000);
s.send(Panel.create("shop", Place.menu("shop")).content(
Ui.text("&lWelcome!"),
Ui.button("boots", "Speed Boots")
.icon(Icon.item("minecraft:diamond_boots"))
.enabledWhen(Condition.ge("coins", 500))
.onClick(click -> click.session().send(
Toast.create("bought").title("Bought Speed Boots").kind(Toast.Kind.SUCCESS)))));
}
}onReady runs when a Leaf client says hello. Everything sent in the same tick goes out in one batch. Listeners and click handlers run on the main server thread, so you can call the Bukkit API in them.
package com.example.network;
import com.google.inject.Inject;
import com.leafclient.connect.sdk.Action;
import com.leafclient.connect.sdk.Panel;
import com.leafclient.connect.sdk.Place;
import com.leafclient.connect.sdk.Ui;
import com.leafclient.connect.sdk.velocity.VelocityConnect;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.proxy.ProxyServer;
import org.slf4j.Logger;
@Plugin(id = "networkbar", name = "NetworkBar", version = "1.0.0")
public final class NetworkBar {
private final ProxyServer proxy;
private final Logger logger;
private VelocityConnect connect;
@Inject
public NetworkBar(ProxyServer proxy, Logger logger) {
this.proxy = proxy;
this.logger = logger;
}
@Subscribe
public void onInit(ProxyInitializeEvent event) {
connect = VelocityConnect.enable(this, proxy, logger);
connect.onReady(s -> s.send(Panel.create("net.bar", Place.chat().order(-1))
.scope(Panel.Scope.CONNECTION)
.content(Ui.row(
Ui.button("net.lobby", "Lobby").on(Action.command("/server lobby")),
Ui.button("net.bedwars", "Bed Wars").on(Action.command("/server bedwars"))))));
}
@Subscribe
public void onShutdown(ProxyShutdownEvent event) {
connect.disable();
}
}Panel.Scope.CONNECTION keeps the bar when the player switches backend servers.
package com.example.network;
import com.leafclient.connect.sdk.Action;
import com.leafclient.connect.sdk.Panel;
import com.leafclient.connect.sdk.Place;
import com.leafclient.connect.sdk.Ui;
import com.leafclient.connect.sdk.bungee.BungeeConnect;
import net.md_5.bungee.api.plugin.Plugin;
public final class NetworkBar extends Plugin {
private BungeeConnect connect;
@Override
public void onEnable() {
connect = BungeeConnect.enable(this);
connect.onReady(s -> s.send(Panel.create("net.bar", Place.chat().order(-1))
.scope(Panel.Scope.CONNECTION)
.content(Ui.row(
Ui.button("net.lobby", "Lobby").on(Action.command("/server lobby")),
Ui.button("net.bedwars", "Bed Wars").on(Action.command("/server bedwars"))))));
}
@Override
public void onDisable() {
connect.disable();
}
}On both proxies, buttons whose only actions are commands send no click to the server. Add Action.send() to their actions if your plugin needs the click. See the platform guides for running a proxy and backends together.
example/ is a complete Bukkit plugin: a menu with a shop, a leaderboard and a sign up form, a coins value, and a panel at the world spawn. It is the plugin in the in-game screenshot above.

