Skip to content

Commit d899242

Browse files
Add MCP SSE Server for Dev UI Json RPC
Signed-off-by: Phillip Kruger <[email protected]>
1 parent e57fa8c commit d899242

File tree

16 files changed

+547
-56
lines changed

16 files changed

+547
-56
lines changed

extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ void registerDevUiHandlers(
159159
routeProducer.produce(
160160
nonApplicationRootPathBuildItem
161161
.routeBuilder().route(DEVUI + SLASH + JSONRPC)
162-
.handler(recorder.communicationHandler())
162+
.handler(recorder.webSocketHandler())
163163
.build());
164164

165165
// Static handler for components
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.quarkus.devui.deployment.mcp;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
6+
import io.quarkus.deployment.IsDevelopment;
7+
import io.quarkus.deployment.annotations.BuildProducer;
8+
import io.quarkus.deployment.annotations.BuildStep;
9+
import io.quarkus.deployment.annotations.ExecutionTime;
10+
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
11+
import io.quarkus.devui.deployment.InternalPageBuildItem;
12+
import io.quarkus.devui.runtime.DevUIRecorder;
13+
import io.quarkus.devui.runtime.mcp.MCPResourcesService;
14+
import io.quarkus.devui.runtime.mcp.MCPToolsService;
15+
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
16+
import io.quarkus.devui.spi.page.Page;
17+
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
18+
import io.quarkus.vertx.http.deployment.RouteBuildItem;
19+
20+
public class MCPProcessor {
21+
22+
private static final String DEVMCP = "dev-mcp";
23+
24+
private static final String NS_MCP = "mcp";
25+
private static final String NS_RESOURCES = "resources";
26+
private static final String NS_TOOLS = "tools";
27+
28+
@BuildStep(onlyIf = IsDevelopment.class)
29+
InternalPageBuildItem createMCPPage(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) {
30+
InternalPageBuildItem mcpServerPage = new InternalPageBuildItem("MCP Server", 28);
31+
32+
// Pages
33+
mcpServerPage.addPage(Page.webComponentPageBuilder()
34+
.namespace(NS_MCP)
35+
.title("MCP Server")
36+
.icon("font-awesome-solid:robot")
37+
.componentLink("qwc-mcp-server.js"));
38+
39+
mcpServerPage.addPage(Page.webComponentPageBuilder()
40+
.namespace(NS_MCP)
41+
.title("Tools")
42+
.icon("font-awesome-solid:screwdriver-wrench")
43+
.componentLink("qwc-mcp-tools.js"));
44+
45+
mcpServerPage.addPage(Page.webComponentPageBuilder()
46+
.namespace(NS_MCP)
47+
.title("Resources")
48+
.icon("font-awesome-solid:file-invoice")
49+
.componentLink("qwc-mcp-resources.js"));
50+
51+
return mcpServerPage;
52+
}
53+
54+
@BuildStep(onlyIf = IsDevelopment.class)
55+
@io.quarkus.deployment.annotations.Record(ExecutionTime.STATIC_INIT)
56+
void registerDevUiHandlers(
57+
BuildProducer<RouteBuildItem> routeProducer,
58+
DevUIRecorder recorder,
59+
LaunchModeBuildItem launchModeBuildItem,
60+
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) throws IOException {
61+
62+
if (launchModeBuildItem.isNotLocalDevModeType()) {
63+
return;
64+
}
65+
66+
// SSE for JsonRPC comms
67+
routeProducer.produce(
68+
nonApplicationRootPathBuildItem
69+
.routeBuilder().route(DEVMCP)
70+
.handler(recorder.serverSendEventHandler())
71+
.build());
72+
73+
}
74+
75+
@BuildStep(onlyIf = IsDevelopment.class)
76+
void createMCPJsonRPCService(BuildProducer<JsonRPCProvidersBuildItem> bp) {
77+
bp.produce(List.of(
78+
new JsonRPCProvidersBuildItem(NS_RESOURCES, MCPResourcesService.class),
79+
new JsonRPCProvidersBuildItem(NS_TOOLS, MCPToolsService.class)));
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { LitElement, html, css} from 'lit';
2+
3+
/**
4+
* This component show details on the MCP Server
5+
*/
6+
export class QwcMCPServer extends LitElement {
7+
static styles = css`
8+
9+
`;
10+
11+
static properties = {
12+
13+
}
14+
15+
constructor() {
16+
super();
17+
}
18+
19+
connectedCallback() {
20+
super.connectedCallback();
21+
}
22+
23+
render() {
24+
return html`Here show details on the MCP Server and how it can be used`;
25+
}
26+
}
27+
customElements.define('qwc-mcp-server', QwcMCPServer);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { LitElement, html, css} from 'lit';
2+
import '@vaadin/progress-bar';
3+
import { JsonRpc } from 'jsonrpc';
4+
import '@vaadin/grid';
5+
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
6+
import '@vaadin/grid/vaadin-grid-sort-column.js';
7+
import '@vaadin/tabs';
8+
import '@vaadin/tabsheet';
9+
import { observeState } from 'lit-element-state';
10+
import { themeState } from 'theme-state';
11+
import '@qomponent/qui-code-block';
12+
13+
/**
14+
* This component show all available tools for MCP clients
15+
*/
16+
export class QwcMCPTools extends observeState(LitElement) {
17+
tools = new JsonRpc("tools");
18+
19+
static styles = css`
20+
21+
`;
22+
23+
static properties = {
24+
_tools: {state: true}
25+
}
26+
27+
constructor() {
28+
super();
29+
30+
}
31+
32+
connectedCallback() {
33+
super.connectedCallback();
34+
this._loadTools();
35+
}
36+
37+
render() {
38+
if (this._tools) {
39+
return html`${this._renderTools()}`;
40+
}else{
41+
return html`
42+
<div style="color: var(--lumo-secondary-text-color);width: 95%;" >
43+
<div>Fetching tools...</div>
44+
<vaadin-progress-bar indeterminate></vaadin-progress-bar>
45+
</div>
46+
`;
47+
}
48+
}
49+
50+
_renderTools(){
51+
return html`<vaadin-tabsheet>
52+
<vaadin-tabs slot="tabs">
53+
<vaadin-tab id="list-tab">List</vaadin-tab>
54+
<vaadin-tab id="raw-tab">Raw json</vaadin-tab>
55+
</vaadin-tabs>
56+
<div tab="list-tab">
57+
<vaadin-grid .items="${this._tools.tools}" all-rows-visible>
58+
<vaadin-grid-sort-column
59+
header='Name'
60+
path="name">
61+
</vaadin-grid-sort-column>
62+
<vaadin-grid-sort-column
63+
header='Description'
64+
path="description">
65+
</vaadin-grid-sort-column>
66+
</vaadin-grid>
67+
</div>
68+
<div tab="raw-tab">
69+
<div class="codeBlock">
70+
<qui-code-block
71+
mode='json'
72+
content='${JSON.stringify(this._tools, null, 2)}'
73+
theme='${themeState.theme.name}'
74+
showLineNumbers>
75+
</qui-code-block>
76+
</div>
77+
</vaadin-tabsheet>
78+
`;
79+
}
80+
81+
_loadTools(){
82+
this.tools.list().then(jsonRpcResponse => {
83+
this._tools = jsonRpcResponse.result;
84+
});
85+
}
86+
87+
}
88+
customElements.define('qwc-mcp-tools', QwcMCPTools);

extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ private JsonMapper createJsonMapper() {
7777
}));
7878
}
7979

80-
public Handler<RoutingContext> communicationHandler() {
80+
public Handler<RoutingContext> webSocketHandler() {
8181
return new DevUIWebSocket();
8282
}
8383

84+
public Handler<RoutingContext> serverSendEventHandler() {
85+
return new DevUIServerSentEvents();
86+
}
87+
8488
public Handler<RoutingContext> uiHandler(String finalDestination,
8589
String path,
8690
List<FileSystemStaticHandler.StaticWebRootConfiguration> webRootConfigurations,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.quarkus.devui.runtime;
2+
3+
import jakarta.enterprise.inject.spi.CDI;
4+
5+
import org.jboss.logging.Logger;
6+
7+
import io.quarkus.devui.runtime.comms.JsonRpcRouter;
8+
import io.vertx.core.Handler;
9+
import io.vertx.core.http.HttpMethod;
10+
import io.vertx.ext.web.RoutingContext;
11+
12+
/**
13+
* Alternative Dev UI Json RPC communication using Server-Sent Events (SSE)
14+
*/
15+
public class DevUIServerSentEvents implements Handler<RoutingContext> {
16+
private static final Logger LOG = Logger.getLogger(DevUIServerSentEvents.class.getName());
17+
18+
@Override
19+
public void handle(RoutingContext ctx) {
20+
LOG.info(ctx.request().method().name() + " : " + ctx.request().absoluteURI());
21+
22+
if (ctx.request().method().equals(HttpMethod.GET)) {
23+
24+
ctx.response()
25+
.putHeader("Content-Type", "text/event-stream; charset=utf-8")
26+
.putHeader("Cache-Control", "no-cache")
27+
.putHeader("Connection", "keep-alive")
28+
.setChunked(true);
29+
30+
try {
31+
JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get();
32+
jsonRpcRouter.addSseSession(ctx);
33+
} catch (IllegalStateException e) {
34+
LOG.debug("Failed to connect to dev sse server", e);
35+
ctx.response().end();
36+
}
37+
}
38+
39+
}
40+
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public void handle(AsyncResult<ServerWebSocket> event) {
2626
ServerWebSocket socket = event.result();
2727
addSocket(socket);
2828
} else {
29-
LOG.debug("Failed to connect to dev ui communication server", event.cause());
29+
LOG.debug("Failed to connect to dev ws server", event.cause());
3030
}
3131
}
3232
});
@@ -40,7 +40,7 @@ private void addSocket(ServerWebSocket session) {
4040
JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get();
4141
jsonRpcRouter.addSocket(session);
4242
} catch (IllegalStateException ise) {
43-
LOG.debug("Failed to connect to dev ui communication server, " + ise.getMessage());
43+
LOG.debug("Failed to connect to dev ws server, " + ise.getMessage());
4444
}
4545
}
4646

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.quarkus.devui.runtime.comms;
2+
3+
public interface JsonRpcResponseWriter {
4+
void write(String message);
5+
6+
void close();
7+
8+
boolean isOpen();
9+
10+
boolean isClosed();
11+
}

0 commit comments

Comments
 (0)