Skip to content

Commit 6178a95

Browse files
authored
Merge pull request #1601 from jmartisk/dev-ui-tools
Dev UI page for MCP clients
2 parents 3767ba1 + 87087d2 commit 6178a95

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkiverse.langchain4j.mcp.deployment.devui;
2+
3+
import io.quarkiverse.langchain4j.mcp.runtime.devui.McpClientsJsonRpcService;
4+
import io.quarkus.deployment.IsDevelopment;
5+
import io.quarkus.deployment.annotations.BuildProducer;
6+
import io.quarkus.deployment.annotations.BuildStep;
7+
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
8+
import io.quarkus.devui.spi.page.CardPageBuildItem;
9+
import io.quarkus.devui.spi.page.Page;
10+
11+
public class McpDevUiProcessor {
12+
13+
@BuildStep(onlyIf = IsDevelopment.class)
14+
CardPageBuildItem cardPage() {
15+
CardPageBuildItem card = new CardPageBuildItem();
16+
card.addPage(Page.webComponentPageBuilder().title("MCP clients")
17+
.componentLink("qwc-mcp-clients.js")
18+
.icon("font-awesome-solid:robot"));
19+
return card;
20+
}
21+
22+
@BuildStep
23+
void jsonRpcService(BuildProducer<JsonRPCProvidersBuildItem> producers) {
24+
producers.produce(new JsonRPCProvidersBuildItem(McpClientsJsonRpcService.class));
25+
}
26+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {LitElement, html, css} from 'lit';
2+
import '@vaadin/text-area';
3+
import '@vaadin/button';
4+
import '@vaadin/checkbox';
5+
import '@vaadin/details';
6+
import '@vaadin/vertical-layout';
7+
import '@vaadin/message-input';
8+
import '@vaadin/message-list';
9+
import '@vaadin/progress-bar';
10+
import '@vaadin/text-field';
11+
import '@vaadin/icon';
12+
import '@vaadin/icons';
13+
import 'qui-alert';
14+
import { JsonRpc } from 'jsonrpc';
15+
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
16+
17+
export class QwcMcpClients extends LitElement {
18+
19+
jsonRpc = new JsonRpc(this);
20+
21+
static styles = css`
22+
:host {
23+
height: 100%;
24+
display: flex;
25+
flex-direction: column;
26+
margin-left: 15px;
27+
margin-right: 15px;
28+
}
29+
`;
30+
31+
static properties = {
32+
_clientInfos: {state: true},
33+
}
34+
35+
constructor() {
36+
super();
37+
this._clientInfos = [];
38+
this.jsonRpc.clientInfos().then(jsonRpcResponse => {
39+
this._clientInfos = jsonRpcResponse.result;
40+
});
41+
}
42+
43+
render() {
44+
if(this._clientInfos === []) {
45+
return html`Loading...`;
46+
} else return html`
47+
${this._clientInfos.map(clientInfo => html`
48+
49+
<h2>MCP client: ${clientInfo.cdiName}</h2>
50+
<h3>Tools</h3>
51+
<vaadin-grid .items="${clientInfo.tools}" theme="wrap-cell-content">
52+
<vaadin-grid-sort-column resizable
53+
path="name"
54+
header="Name">
55+
</vaadin-grid-sort-column>
56+
<vaadin-grid-sort-column resizable
57+
path="description"
58+
header="Description">
59+
</vaadin-grid-sort-column>
60+
<vaadin-grid-sort-column resizable
61+
header="Execute"
62+
${columnBodyRenderer((tool => this._toolExecuteColumnRenderer(tool, clientInfo)), [])}>
63+
</vaadin-grid-sort-column>
64+
</vaadin-grid>
65+
`)}
66+
`;
67+
}
68+
69+
_toolExecuteColumnRenderer(tool, clientInfo) {
70+
var actualArguments = tool.exampleInput;
71+
const textAreaId = `output-${clientInfo.cdiName}-${tool.name}`
72+
return html`
73+
<vaadin-vertical-layout>
74+
<vaadin-text-area label="Arguments" value=${tool.exampleInput} style="width: 100%;"
75+
@change=${(e) => actualArguments = e.detail.sourceEvent.currentTarget.value}></vaadin-text-area>
76+
<vaadin-button @click=${() => {
77+
this.jsonRpc.executeTool({clientName: clientInfo.cdiName, toolName: tool.name, arguments: actualArguments})
78+
.then(jsonRpcResponse => {
79+
let outputElement = this.shadowRoot.getElementById(textAreaId);
80+
outputElement.style = "width: 100%; display: block;";
81+
outputElement.value = JSON.stringify(jsonRpcResponse.result);
82+
});}
83+
}>Execute</vaadin-button>
84+
<vaadin-text-area label="Output" id=${textAreaId} disabled style="width: 100%; display: none;"></vaadin-text-area>
85+
</vaadin-vertical-layout>
86+
`
87+
}
88+
89+
}
90+
91+
customElements.define('qwc-mcp-clients', QwcMcpClients);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.quarkiverse.langchain4j.mcp.runtime.devui;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.util.ArrayList;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
import jakarta.enterprise.inject.Any;
10+
11+
import dev.langchain4j.agent.tool.ToolExecutionRequest;
12+
import dev.langchain4j.agent.tool.ToolSpecification;
13+
import dev.langchain4j.mcp.client.McpClient;
14+
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
15+
import io.quarkiverse.langchain4j.mcp.runtime.devui.json.JsonSchemaToExampleStringHelper;
16+
import io.quarkiverse.langchain4j.mcp.runtime.devui.json.McpClientInfo;
17+
import io.quarkiverse.langchain4j.mcp.runtime.devui.json.McpToolInfo;
18+
import io.quarkus.arc.Arc;
19+
import io.quarkus.arc.InjectableBean;
20+
import io.quarkus.arc.InstanceHandle;
21+
22+
public class McpClientsJsonRpcService {
23+
24+
// The key here is the logical (CDI) name of the client
25+
private final Map<String, McpClient> clients = new HashMap<>();
26+
27+
public McpClientsJsonRpcService() {
28+
// initialize the client map
29+
for (InstanceHandle<McpClient> handle : Arc.container().select(McpClient.class, Any.Literal.INSTANCE).handles()) {
30+
InjectableBean<McpClient> bean = handle.getBean();
31+
String key = null;
32+
for (Annotation qualifier : bean.getQualifiers()) {
33+
if (qualifier instanceof McpClientName mcpClientName) {
34+
key = mcpClientName.value();
35+
break;
36+
}
37+
}
38+
// TODO: if no CDI key exists, generate one ad-hoc for the purpose of having the client uniquely identifiable in the UI?
39+
clients.put(key == null ? "null" : key, handle.get());
40+
}
41+
}
42+
43+
public List<McpClientInfo> clientInfos() {
44+
List<McpClientInfo> infos = new ArrayList<>();
45+
for (String key : clients.keySet()) {
46+
McpClient client = clients.get(key);
47+
McpClientInfo info = buildClientInfo(client);
48+
info.setCdiName(key);
49+
infos.add(info);
50+
}
51+
return infos;
52+
}
53+
54+
public String executeTool(String clientName, String toolName, String arguments) {
55+
ToolExecutionRequest request = ToolExecutionRequest.builder()
56+
.name(toolName)
57+
.arguments(arguments)
58+
.build();
59+
return clients.get(clientName).executeTool(request);
60+
}
61+
62+
private McpClientInfo buildClientInfo(McpClient client) {
63+
McpClientInfo info = new McpClientInfo();
64+
info.setTools(client.listTools().stream().map(this::buildToolInfo).toList());
65+
return info;
66+
}
67+
68+
private McpToolInfo buildToolInfo(ToolSpecification toolSpec) {
69+
McpToolInfo info = new McpToolInfo();
70+
info.setName(toolSpec.name());
71+
info.setDescription(toolSpec.description());
72+
info.setExampleInput(JsonSchemaToExampleStringHelper.generateExampleStringFromSchema(toolSpec.parameters()));
73+
return info;
74+
}
75+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.quarkiverse.langchain4j.mcp.runtime.devui.json;
2+
3+
import java.util.stream.Collectors;
4+
5+
import dev.langchain4j.model.chat.request.json.*;
6+
7+
public class JsonSchemaToExampleStringHelper {
8+
9+
public static String generateExampleStringFromSchema(JsonSchemaElement element) {
10+
if (element instanceof JsonBooleanSchema) {
11+
return "true";
12+
} else if (element instanceof JsonNumberSchema) {
13+
return "1.0";
14+
} else if (element instanceof JsonStringSchema) {
15+
return "\"example\"";
16+
} else if (element instanceof JsonIntegerSchema) {
17+
return "1";
18+
} else if (element instanceof JsonNullSchema) {
19+
return "null";
20+
} else if (element instanceof JsonObjectSchema schema) {
21+
StringBuilder builder = new StringBuilder();
22+
builder.append("{");
23+
String example = schema.properties().entrySet().stream()
24+
.map((entry) -> "\"" + entry.getKey() + "\": " + generateExampleStringFromSchema(entry.getValue()))
25+
.collect(Collectors.joining(", "));
26+
builder.append(example);
27+
builder.append("}");
28+
return builder.toString();
29+
} else if (element instanceof JsonArraySchema schema) {
30+
StringBuilder builder = new StringBuilder();
31+
builder.append("[");
32+
builder.append(generateExampleStringFromSchema(schema.items()));
33+
builder.append("]");
34+
return builder.toString();
35+
} else if (element instanceof JsonAnyOfSchema schema) {
36+
return generateExampleStringFromSchema(schema.anyOf().get(0));
37+
}
38+
throw new UnsupportedOperationException("Unsupported schema type: " + element.getClass());
39+
}
40+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.quarkiverse.langchain4j.mcp.runtime.devui.json;
2+
3+
import java.util.List;
4+
5+
public class McpClientInfo {
6+
7+
private String cdiName;
8+
private List<McpToolInfo> tools;
9+
10+
public String getCdiName() {
11+
return cdiName;
12+
}
13+
14+
public void setCdiName(String cdiName) {
15+
this.cdiName = cdiName;
16+
}
17+
18+
public List<McpToolInfo> getTools() {
19+
return tools;
20+
}
21+
22+
public void setTools(List<McpToolInfo> tools) {
23+
this.tools = tools;
24+
}
25+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.quarkiverse.langchain4j.mcp.runtime.devui.json;
2+
3+
public class McpToolInfo {
4+
5+
private String name;
6+
private String description;
7+
private String exampleInput;
8+
9+
public String getName() {
10+
return name;
11+
}
12+
13+
public void setName(String name) {
14+
this.name = name;
15+
}
16+
17+
public String getExampleInput() {
18+
return exampleInput;
19+
}
20+
21+
public void setExampleInput(String exampleInput) {
22+
this.exampleInput = exampleInput;
23+
}
24+
25+
public String getDescription() {
26+
return description;
27+
}
28+
29+
public void setDescription(String description) {
30+
this.description = description;
31+
}
32+
}

0 commit comments

Comments
 (0)