Skip to content

Commit b8d457e

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

21 files changed

+1026
-91
lines changed

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import io.quarkus.devui.deployment.extension.Codestart;
4848
import io.quarkus.devui.deployment.extension.Extension;
4949
import io.quarkus.devui.deployment.jsonrpc.DevUIDatabindCodec;
50+
import io.quarkus.devui.runtime.DevUIBuildTimeStaticService;
5051
import io.quarkus.devui.runtime.DevUICORSFilter;
5152
import io.quarkus.devui.runtime.DevUIRecorder;
5253
import io.quarkus.devui.runtime.VertxRouteInfoService;
@@ -129,6 +130,7 @@ public class DevUIProcessor {
129130
@Record(ExecutionTime.STATIC_INIT)
130131
void registerDevUiHandlers(
131132
DevUIConfig devUIConfig,
133+
BeanContainerBuildItem beanContainer,
132134
MvnpmBuildItem mvnpmBuildItem,
133135
List<DevUIRoutesBuildItem> devUIRoutesBuildItems,
134136
List<StaticContentBuildItem> staticContentBuildItems,
@@ -159,7 +161,7 @@ void registerDevUiHandlers(
159161
routeProducer.produce(
160162
nonApplicationRootPathBuildItem
161163
.routeBuilder().route(DEVUI + SLASH + JSONRPC)
162-
.handler(recorder.communicationHandler())
164+
.handler(recorder.webSocketHandler())
163165
.build());
164166

165167
// Static handler for components
@@ -204,7 +206,8 @@ void registerDevUiHandlers(
204206

205207
urlAndPath.put(c.getFileName(), tempFile.toString());
206208
}
207-
Handler<RoutingContext> buildTimeStaticHandler = recorder.buildTimeStaticHandler(basepath, urlAndPath);
209+
Handler<RoutingContext> buildTimeStaticHandler = recorder.buildTimeStaticHandler(beanContainer.getValue(), basepath,
210+
urlAndPath);
208211

209212
routeProducer.produce(
210213
nonApplicationRootPathBuildItem.routeBuilder().route(DEVUI + SLASH_ALL)
@@ -270,7 +273,6 @@ void additionalBean(BuildProducer<AdditionalBeanBuildItem> additionalBeanProduce
270273
List<JsonRPCProvidersBuildItem> jsonRPCProvidersBuildItems) {
271274

272275
additionalBeanProducer.produce(AdditionalBeanBuildItem.builder()
273-
.addBeanClass(JsonRpcRouter.class)
274276
.addBeanClass(VertxRouteInfoService.class)
275277
.setUnremovable().build());
276278

@@ -294,6 +296,11 @@ void additionalBean(BuildProducer<AdditionalBeanBuildItem> additionalBeanProduce
294296
.setDefaultScope(BuiltinScope.APPLICATION.getName())
295297
.setUnremovable().build());
296298

299+
additionalBeanProducer.produce(AdditionalBeanBuildItem.builder()
300+
.addBeanClass(DevUIBuildTimeStaticService.class)
301+
.setDefaultScope(BuiltinScope.APPLICATION.getName())
302+
.setUnremovable().build());
303+
297304
}
298305

299306
/**
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,158 @@
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+
import '@vaadin/dialog';
13+
import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js';
14+
15+
/**
16+
* This component show all available resources for MCP clients
17+
*/
18+
export class QwcMCPResources extends observeState(LitElement) {
19+
jsonRpc = new JsonRpc("resources");
20+
21+
static styles = css`
22+
23+
`;
24+
25+
static properties = {
26+
_resources: {state: true},
27+
_selectedResource: {state: true},
28+
_selectedResourceContent: {state: true},
29+
_busyReading: {state: true}
30+
}
31+
32+
constructor() {
33+
super();
34+
this._selectedResource = [];
35+
this._selectedResourceContent = null;
36+
this._busyReading = false;
37+
}
38+
39+
connectedCallback() {
40+
super.connectedCallback();
41+
this._loadResources();
42+
}
43+
44+
render() {
45+
if (this._resources) {
46+
return html`${this._renderResources()}`;
47+
}else{
48+
return this._renderProgressBar("Fetching resources...");
49+
}
50+
}
51+
52+
_renderResources(){
53+
return html`
54+
<vaadin-dialog
55+
header-title="Read resource"
56+
.opened="${this._selectedResourceContent!==null}"
57+
@opened-changed="${(event) => {
58+
if(!event.detail.value){
59+
this._selectedResource = [];
60+
this._selectedResourceContent = null;
61+
}
62+
}}"
63+
${dialogHeaderRenderer(
64+
() => html`
65+
<vaadin-button theme="tertiary" @click="${this._closeDialog}">
66+
<vaadin-icon icon="font-awesome-solid:xmark"></vaadin-icon>
67+
</vaadin-button>
68+
`,
69+
[]
70+
)}
71+
${dialogRenderer(() => this._renderResourceContent(), this._selectedResource)}
72+
></vaadin-dialog>
73+
<vaadin-tabsheet>
74+
<vaadin-tabs slot="tabs">
75+
<vaadin-tab id="list-tab">List</vaadin-tab>
76+
<vaadin-tab id="raw-tab">Raw json</vaadin-tab>
77+
</vaadin-tabs>
78+
<div tab="list-tab">
79+
<vaadin-grid .items="${this._resources.resources}" .selectedItems="${this._selectedResource}" all-rows-visible
80+
@active-item-changed="${(e) => {
81+
const item = e.detail.value;
82+
this._selectedResource = item ? [item] : [];
83+
84+
if(this._selectedResource.length>0){
85+
this._readSelectedResourceContents();
86+
}else{
87+
this._selectedResourceContent = null;
88+
}
89+
}}">
90+
<vaadin-grid-sort-column
91+
header='Name'
92+
path="name">
93+
</vaadin-grid-sort-column>
94+
<vaadin-grid-sort-column
95+
header='Description'
96+
path="description">
97+
</vaadin-grid-sort-column>
98+
</vaadin-grid>
99+
</div>
100+
<div tab="raw-tab">
101+
<div class="codeBlock">
102+
<qui-code-block
103+
mode='json'
104+
content='${JSON.stringify(this._resources, null, 2)}'
105+
theme='${themeState.theme.name}'
106+
showLineNumbers>
107+
</qui-code-block>
108+
</div>
109+
</vaadin-tabsheet>
110+
`;
111+
}
112+
113+
_renderResourceContent(){
114+
return html`<div class="codeBlock">
115+
<qui-code-block
116+
mode='json'
117+
content='${this._selectedResourceContent}'
118+
theme='${themeState.theme.name}'
119+
showLineNumbers>
120+
</qui-code-block>`;
121+
}
122+
123+
_loadResources(){
124+
this.jsonRpc.list().then(jsonRpcResponse => {
125+
this._resources = jsonRpcResponse.result;
126+
});
127+
}
128+
129+
_closeDialog(){
130+
this._selectedResourceContent = null;
131+
}
132+
133+
_readSelectedResourceContents(){
134+
if(this._selectedResource.length>0 && !this._busyReading){
135+
136+
this._busyReading = true;
137+
this.jsonRpc.read({uri:this._selectedResource[0].uri}).then(jsonRpcResponse => {
138+
139+
if(jsonRpcResponse.result.contents.length>0){
140+
this._selectedResourceContent = jsonRpcResponse.result.contents[0].text;
141+
}else{
142+
this._selectedResourceContent = "No data found";
143+
}
144+
this._busyReading = false;
145+
});
146+
}
147+
}
148+
149+
_renderProgressBar(title){
150+
return html`
151+
<div style="color: var(--lumo-secondary-text-color);width: 95%;" >
152+
<div>${title}</div>
153+
<vaadin-progress-bar indeterminate></vaadin-progress-bar>
154+
</div>`;
155+
}
156+
157+
}
158+
customElements.define('qwc-mcp-resources', QwcMCPResources);
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` TODO: Here show details on the MCP Server and how it can be used`;
25+
}
26+
}
27+
customElements.define('qwc-mcp-server', QwcMCPServer);

0 commit comments

Comments
 (0)