Skip to content

Commit dd8ea41

Browse files
committed
jooby-mcp: Add kliushnichenko mcp runtime
1 parent df7590a commit dd8ea41

30 files changed

Lines changed: 2916 additions & 29 deletions

jooby/src/main/java/io/jooby/annotation/McpCompletion.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,4 @@
1919
* Resource Template URI (e.g., "file:///project/{name}").
2020
*/
2121
String ref();
22-
23-
/** The name of the argument or template variable being completed. */
24-
String arg();
2522
}

modules/jooby-jackson3/src/main/java/io/jooby/jackson3/Jackson3Module.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public Object decode(Context ctx, Type type) throws Exception {
236236
* @param modules Extra/additional modules to install.
237237
* @return Object mapper instance.
238238
*/
239-
public static ObjectMapper create(JacksonModule... modules) {
239+
public static JsonMapper create(JacksonModule... modules) {
240240
JsonMapper.Builder builder = JsonMapper.builder();
241241

242242
Stream.of(modules).forEach(builder::addModule);

modules/jooby-mcp/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818
<artifactId>jooby</artifactId>
1919
<version>${jooby.version}</version>
2020
</dependency>
21+
<dependency>
22+
<groupId>com.fasterxml.jackson.core</groupId>
23+
<artifactId>jackson-databind</artifactId>
24+
</dependency>
2125
<dependency>
2226
<groupId>io.modelcontextprotocol.sdk</groupId>
2327
<artifactId>mcp-core</artifactId>
2428
</dependency>
29+
<dependency>
30+
<groupId>io.modelcontextprotocol.sdk</groupId>
31+
<artifactId>mcp-json-jackson2</artifactId>
32+
</dependency>
2533
</dependencies>
2634
</project>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// This file is generated by McpToolProcessor. Do not modify manually.
2+
package io.github.kliushnichenko.jooby.mcp.example;
3+
4+
import io.github.kliushnichenko.jooby.mcp.JoobyMcpServer;
5+
import io.github.kliushnichenko.jooby.mcp.ResourceUri;
6+
import io.github.kliushnichenko.jooby.mcp.internal.MethodInvoker;
7+
import io.github.kliushnichenko.jooby.mcp.internal.ToolSpec;
8+
import io.jooby.Jooby;
9+
import io.modelcontextprotocol.json.McpJsonMapper;
10+
import io.modelcontextprotocol.server.McpSyncServerExchange;
11+
import io.modelcontextprotocol.spec.McpSchema;
12+
import java.lang.Object;
13+
import java.lang.String;
14+
import java.util.ArrayList;
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.function.Function;
19+
import java.util.function.Supplier;
20+
21+
/**
22+
* Generated Jooby MCP Server. Do not modify manually.
23+
*/
24+
public class ExampleMcpServer implements JoobyMcpServer {
25+
private Jooby app;
26+
27+
private McpJsonMapper mcpJsonMapper;
28+
29+
/**
30+
* Map of tool names to its specification.
31+
*/
32+
private final Map<String, ToolSpec> tools = new HashMap<>();
33+
34+
/**
35+
* Map of tool names to method invokers.
36+
*/
37+
private final Map<String, MethodInvoker> toolInvokers = new HashMap<>();
38+
39+
/**
40+
* Map of prompt names to its specification.
41+
*/
42+
private final Map<String, McpSchema.Prompt> prompts = new HashMap<>();
43+
44+
/**
45+
* Map of prompt names to method invokers.
46+
*/
47+
private final Map<String, MethodInvoker> promptInvokers = new HashMap<>();
48+
49+
/**
50+
* List of completions reference objects.
51+
*/
52+
private final List<McpSchema.CompleteReference> completions = new ArrayList<>();
53+
54+
/**
55+
* Map of completion key(a composition of <identifier>_<argumentName>) to method invoker.
56+
*/
57+
private final Map<String, Function<String, Object>> completionInvokers = new HashMap<>();
58+
59+
/**
60+
* List of resources.
61+
*/
62+
private final List<McpSchema.Resource> resources = new ArrayList<>();
63+
64+
/**
65+
* Map of resource URI to method invoker.
66+
*/
67+
private final Map<String, Supplier<Object>> resourceReaders = new HashMap<>();
68+
69+
/**
70+
* List of resource templates.
71+
*/
72+
private final List<McpSchema.ResourceTemplate> resourceTemplates = new ArrayList<>();
73+
74+
/**
75+
* Map of resource URI template to method invoker.
76+
*/
77+
private final Map<String, Function<Map<String, Object>, Object>> resourceTemplateReaders = new HashMap<>();
78+
79+
/**
80+
* Initialize a new server.
81+
* @param app the Jooby application instance
82+
* @param mcpJsonMapper json serializer instance
83+
*/
84+
public void init(final Jooby app, final McpJsonMapper mcpJsonMapper) {
85+
this.app = app;
86+
this.mcpJsonMapper = mcpJsonMapper;
87+
88+
tools.put("elicitation_example", ToolSpec.builder().name("elicitation_example").description("Request the username over elicitation").inputSchema("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").requiredArguments(List.of()).build());
89+
tools.put("add", ToolSpec.builder().name("add").description("Adds two numbers together").inputSchema("{\"type\":\"object\",\"properties\":{\"first\":{\"type\":\"integer\",\"description\":\"First number to add\"},\"second\":{\"type\":\"integer\",\"description\":\"Second number to add\"}},\"required\":[\"first\",\"second\"],\"additionalProperties\":false}").outputSchema("{\"type\":\"object\",\"properties\":{\"operation\":{\"type\":\"string\"},\"result\":{\"type\":\"number\"},\"expression\":{\"type\":\"string\"}},\"required\":[\"operation\",\"result\",\"expression\"],\"additionalProperties\":false}").requiredArguments(List.of("first", "second")).build());
90+
tools.put("subtract", ToolSpec.builder().name("subtract").inputSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"integer\"},\"b\":{\"type\":\"integer\"}},\"required\":[\"a\",\"b\"],\"additionalProperties\":false}").requiredArguments(List.of("a", "b")).build());
91+
tools.put("pi_sign_image", ToolSpec.builder().name("pi_sign_image").description("Returns an image of the Pi").inputSchema("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").requiredArguments(List.of()).build());
92+
tools.put("get_client_info", ToolSpec.builder().name("get_client_info").description("Returns the information about the client initiated the request").inputSchema("{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}").requiredArguments(List.of()).build());
93+
94+
toolInvokers.put("elicitation_example", (args, exchange) -> app.require(ElicitationExample.class).requestUsername(exchange));
95+
toolInvokers.put("add", (args, exchange) -> app.require(ToolsExample.class).add((int) args.get("first"), (int) args.get("second")));
96+
toolInvokers.put("subtract", (args, exchange) -> app.require(ToolsExample.class).subtract((int) args.get("a"), (int) args.get("b"), exchange));
97+
toolInvokers.put("pi_sign_image", (args, exchange) -> app.require(ToolsExample.class).getPiSignImage());
98+
toolInvokers.put("get_client_info", (args, exchange) -> app.require(ToolsExample.class).getClientInfo(exchange));
99+
100+
prompts.put("summarizeText", new McpSchema.Prompt("summarizeText", "", "Summarizes the provided text into a specified number of sentences", List.of(new McpSchema.PromptArgument("text_to_summarize", null, true), new McpSchema.PromptArgument("maxSentences", null, true))));
101+
prompts.put("code_review", new McpSchema.Prompt("code_review", "", "Code Review Prompt", List.of(new McpSchema.PromptArgument("codeSnippet", null, true), new McpSchema.PromptArgument("language", null, true), new McpSchema.PromptArgument("scrutinyLevel", null, true))));
102+
103+
promptInvokers.put("summarizeText", (args, exchange) -> app.require(PromptsExample.class).summarizeText((String) args.get("text_to_summarize"), (String) args.get("maxSentences")));
104+
promptInvokers.put("code_review", (args, exchange) -> app.require(PromptsExample.class).codeReviewPrompt((String) args.get("codeSnippet"), (String) args.get("language"), (String) args.get("scrutinyLevel")));
105+
completions.add(new McpSchema.PromptReference("code_review"));
106+
completions.add(new McpSchema.PromptReference("code_review"));
107+
completions.add(new McpSchema.ResourceReference("file:///project/{name}"));
108+
109+
completionInvokers.put("code_review_language", (input) -> app.require(PromptsExample.class).completeCodeReviewLang(input));
110+
completionInvokers.put("code_review_scrutinyLevel", (input) -> app.require(PromptsExample.class).completeScrutinyLevel(input));
111+
completionInvokers.put("file:///project/{name}_name", (input) -> app.require(ResourceTemplateExamples.class).projectNameCompletion(input));
112+
113+
resources.add(McpSchema.Resource.builder().name("README.md").title("README.md").uri("file:///project/README.md").mimeType("text/markdown").build());
114+
resources.add(McpSchema.Resource.builder().name("blobResource").uri("file:///blob").build());
115+
resources.add(McpSchema.Resource.builder().name("threadStone").uri("file:///project/thread-stone").size(Long.valueOf(10563)).annotations(new McpSchema.Annotations(List.of(McpSchema.Role.USER, McpSchema.Role.ASSISTANT), 0.3, null)).build());
116+
resources.add(McpSchema.Resource.builder().name("blackBriar").uri("file:///project/blackbriar/metadata.json").build());
117+
118+
resourceReaders.put("file:///project/README.md", () -> app.require(ResourceExamples.class).textResource());
119+
resourceReaders.put("file:///blob", () -> app.require(ResourceExamples.class).blobResource());
120+
resourceReaders.put("file:///project/thread-stone", () -> app.require(ResourceExamples.class).threadStone());
121+
resourceReaders.put("file:///project/blackbriar/metadata.json", () -> app.require(ResourceExamples.class).blackBriar());
122+
123+
resourceTemplates.add(McpSchema.ResourceTemplate.builder().name("get_project").uriTemplate("file:///project/{name}").build());
124+
125+
resourceTemplateReaders.put("file:///project/{name}", (args) -> app.require(ResourceTemplateExamples.class).getProject((String) args.get("name"), new ResourceUri((String) args.get("__resourceUri"))));
126+
127+
}
128+
129+
/**
130+
* Invokes a tool by name with the provided arguments.
131+
* @param toolName the name of the tool to invoke
132+
* @param args the arguments to pass to the tool
133+
* @return the result of the tool invocation
134+
*/
135+
public Object invokeTool(final String toolName, final Map<String, Object> args,
136+
final McpSyncServerExchange exchange) {
137+
MethodInvoker invoker = toolInvokers.get(toolName);
138+
return invoker.invoke(args, exchange);
139+
}
140+
141+
/**
142+
* Invokes a prompt by name with the provided arguments.
143+
* @param promptName the name of the prompt to invoke
144+
* @param args the arguments to pass to the prompt
145+
* @return the result of the prompt invocation
146+
*/
147+
public Object invokePrompt(final String promptName, final Map<String, Object> args,
148+
final McpSyncServerExchange exchange) {
149+
MethodInvoker invoker = promptInvokers.get(promptName);
150+
return invoker.invoke(args, exchange);
151+
}
152+
153+
/**
154+
* Invokes a completion by identifier(prompt or resource name) and argumentName with the provided
155+
* argument value.
156+
* @param identifier prompt or resource template name
157+
* @param argumentName the name of an argument in prompt or resource template
158+
* @param input incoming argument value
159+
* @return the result of the completion invocation
160+
*/
161+
public Object invokeCompletion(final String identifier, final String argumentName,
162+
final String input) {
163+
var completionKey = identifier + '_' + argumentName;
164+
var invoker = completionInvokers.get(completionKey);
165+
if (invoker == null) {
166+
return List.of();
167+
}
168+
return invoker.apply(input);
169+
}
170+
171+
/**
172+
* Reads a resource by URI
173+
* @param uri Resource URI
174+
* @return resource content
175+
*/
176+
public Object readResource(final String uri) {
177+
var reader = resourceReaders.get(uri);
178+
return reader.get();
179+
}
180+
181+
/**
182+
* Reads a resource by URI according to template
183+
* @param uriTemplate Resource URI template
184+
* @return resource content
185+
*/
186+
public Object readResourceByTemplate(final String uriTemplate, final Map<String, Object> args) {
187+
var reader = resourceTemplateReaders.get(uriTemplate);
188+
return reader.apply(args);
189+
}
190+
191+
public Map<String, ToolSpec> getTools() {
192+
return tools;
193+
}
194+
195+
public Map<String, McpSchema.Prompt> getPrompts() {
196+
return prompts;
197+
}
198+
199+
public List<McpSchema.CompleteReference> getCompletions() {
200+
return completions;
201+
}
202+
203+
public List<McpSchema.Resource> getResources() {
204+
return resources;
205+
}
206+
207+
public List<McpSchema.ResourceTemplate> getResourceTemplates() {
208+
return resourceTemplates;
209+
}
210+
211+
public String getServerKey() {
212+
return "example";
213+
}
214+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.github.kliushnichenko.jooby.mcp.example;
2+
3+
import io.github.kliushnichenko.jooby.mcp.annotation.OutputSchema;
4+
import io.github.kliushnichenko.jooby.mcp.annotation.Tool;
5+
import io.github.kliushnichenko.jooby.mcp.annotation.ToolArg;
6+
import io.modelcontextprotocol.server.McpSyncServerExchange;
7+
import io.modelcontextprotocol.spec.McpSchema;
8+
import jakarta.inject.Singleton;
9+
10+
/**
11+
* @author kliushnichenko
12+
*/
13+
@Singleton
14+
public class ToolsExample {
15+
16+
private static final String PI_SIGN_IMAGE = "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";
17+
18+
public record ArithmeticResult(String operation, double result, String expression) {
19+
}
20+
21+
@Tool(name = "add", description = "Adds two numbers together")
22+
public ArithmeticResult add(
23+
@ToolArg(name = "first", description = "First number to add") int a,
24+
@ToolArg(name = "second", description = "Second number to add") int b
25+
) {
26+
int result = a + b;
27+
return new ArithmeticResult("addition", result, a + " + " + b + " = " + result);
28+
}
29+
30+
@Tool
31+
public String subtract(int a, int b, McpSyncServerExchange exchange) {
32+
int result = a - b;
33+
return String.valueOf(result);
34+
}
35+
36+
@Tool(name = "pi_sign_image", description = "Returns an image of the Pi")
37+
public McpSchema.ImageContent getPiSignImage() {
38+
return new McpSchema.ImageContent(null, PI_SIGN_IMAGE, "image/png");
39+
}
40+
41+
@Tool(name = "get_client_info", description = "Returns the information about the client initiated the request")
42+
@OutputSchema.Suppressed
43+
public McpSchema.Implementation getClientInfo(McpSyncServerExchange exchange) {
44+
return exchange.getClientInfo();
45+
}
46+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.github.kliushnichenko.jooby.mcp.example;
2+
3+
import io.github.kliushnichenko.jooby.mcp.annotation.McpServer;
4+
import io.github.kliushnichenko.jooby.mcp.annotation.Tool;
5+
import jakarta.inject.Singleton;
6+
import lombok.RequiredArgsConstructor;
7+
8+
/**
9+
* @author kliushnichenko
10+
*/
11+
@Singleton
12+
@McpServer("weather")
13+
@RequiredArgsConstructor
14+
public class WeatherServer {
15+
16+
private final WeatherService weatherService;
17+
18+
@Tool(name = "get_weather")
19+
public String getWeather(double latitude, double longitude) {
20+
return weatherService.getWeather(latitude, longitude);
21+
}
22+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.github.kliushnichenko.jooby.mcp.example;
2+
3+
import jakarta.inject.Singleton;
4+
5+
@Singleton
6+
public class WeatherService {
7+
8+
public String getWeather(double latitude, double longitude) {
9+
// Simulate fetching weather data for the given location
10+
// In a real application, this would involve calling a weather API
11+
return "The weather in Numenor is sunny with a temperature of 25°C.";
12+
}
13+
}

0 commit comments

Comments
 (0)