Skip to content

Commit f558d3e

Browse files
authored
Add resolved parameters for Resource templates (#38)
1 parent 9ebe34c commit f558d3e

File tree

18 files changed

+666
-69
lines changed

18 files changed

+666
-69
lines changed

codegen/src/main/java/io/helidon/extensions/mcp/codegen/McpCodegen.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,11 @@ private void generateResources(TypeName generatedType, ClassModel.Builder classM
339339
.name(innerTypeName.className())
340340
.addInterface(MCP_RESOURCE_INTERFACE)
341341
.accessModifier(AccessModifier.PRIVATE)
342+
.addMethod(method -> addResourceUriMethod(method, uri))
342343
.addMethod(method -> addResourceNameMethod(method, element))
343344
.addMethod(method -> addResourceDescriptionMethod(method, description))
344-
.addMethod(method -> addResourceUriMethod(method, uri))
345-
.addMethod(method -> addResourceMediaTypeMethod(method, mediaTypeContent))
346-
.addMethod(method -> addResourceMethod(method, classModel, element)));
345+
.addMethod(method -> addResourceMethod(method, uri, classModel, element))
346+
.addMethod(method -> addResourceMediaTypeMethod(method, mediaTypeContent)));
347347
}
348348
}
349349

@@ -382,13 +382,13 @@ private void addResourceMediaTypeMethod(Method.Builder builder, String mediaType
382382
.addContentLine(".create(\"" + mediaTypeContent + "\");");
383383
}
384384

385-
private void addResourceMethod(Method.Builder builder, ClassModel.Builder classModel, TypedElementInfo element) {
385+
private void addResourceMethod(Method.Builder builder, String uri, ClassModel.Builder classModel, TypedElementInfo element) {
386386
List<String> parameters = new ArrayList<>();
387387
TypeName returnType = element.signature().type();
388388

389389
builder.name("resource")
390-
.returnType(returned -> returned.type(FUNCTION_REQUEST_LIST_RESOURCE_CONTENT))
391-
.addAnnotation(Annotations.OVERRIDE);
390+
.addAnnotation(Annotations.OVERRIDE)
391+
.returnType(returned -> returned.type(FUNCTION_REQUEST_LIST_RESOURCE_CONTENT));
392392
builder.addContentLine("return request -> {");
393393

394394
for (TypedElementInfo parameter : element.parameterArguments()) {
@@ -406,11 +406,25 @@ private void addResourceMethod(Method.Builder builder, ClassModel.Builder classM
406406
}
407407
if (MCP_PROGRESS.equals(parameter.typeName())) {
408408
parameters.add("request.features().progress()");
409+
continue;
410+
}
411+
if (isResourceTemplate(uri)) {
412+
if (MCP_PARAMETERS.equals(parameter.typeName())) {
413+
parameters.add("request.parameters()");
414+
continue;
415+
}
416+
if (TypeNames.STRING.equals(parameter.typeName())) {
417+
parameters.add(parameter.elementName());
418+
builder.addContent("String ")
419+
.addContent(parameter.elementName())
420+
.addContent(" = request.parameters().get(\"")
421+
.addContent(parameter.elementName())
422+
.addContentLine("\").asString().orElse(\"\");");
423+
}
409424
}
410425
}
411426
String params = String.join(", ", parameters);
412427
if (returnType.equals(TypeNames.STRING)) {
413-
classModel.addImport(MCP_RESOURCE_CONTENTS);
414428
builder.addContent("return ")
415429
.addContent(List.class)
416430
.addContent(".of(")
@@ -860,6 +874,10 @@ private boolean isIgnoredSchemaElement(TypeName typeName) {
860874
|| MCP_LOGGER.equals(typeName);
861875
}
862876

877+
private boolean isResourceTemplate(String uri) {
878+
return uri.contains("{") || uri.contains("}");
879+
}
880+
863881
private void initializeComponents() {
864882
components.put(McpKind.TOOL, new LinkedList<>());
865883
components.put(McpKind.PROMPT, new LinkedList<>());

docs/mcp-declarative/README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,7 @@ McpPromptContent image = McpPromptContents.imageContent("base64", MediaTypes.cre
205205

206206
`Resources` allow servers to share data that provides context to language models, such as files, database schemas, or
207207
application-specific information. Clients can list and read them. Resources are identified by name, description, and media type.
208-
209-
**Resource Templates** use [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) to define parameterized URIs. They help
210-
clients discover dynamic content; note that templates are not directly readable.
211-
Define both resources and templates using `@Mcp.Resource`:
208+
Define resources using `@Mcp.Resource`:
212209

213210
```java
214211
@Mcp.Server
@@ -243,6 +240,32 @@ class Server {
243240
}
244241
```
245242

243+
### Resource Templates
244+
245+
Resource Templates utilize [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) to facilitate dynamic resource discovery.
246+
The URI template is matched against the corresponding URI in the client request. To define a resource or template, the same
247+
API as `McpResource` is employed. Parameters enclosed in `{}` denote template variables, which can be accessed via `McpParameters`
248+
using keys that correspond to these variables.
249+
250+
#### Configuration
251+
252+
Use `String` return types for text-only resources. The `@Mcp.Name` annotation lets you override the default resource name.
253+
254+
```java
255+
@Mcp.Server
256+
class Server {
257+
258+
@Mcp.Resource(
259+
uri = "file://{path}",
260+
description = "Resource description",
261+
mediaType = MediaTypes.TEXT_PLAIN_VALUE)
262+
@Mcp.Name("MyResource")
263+
String resource(String path) {
264+
return "File at path " + path + " does not exist.";
265+
}
266+
}
267+
```
268+
246269
#### Resource Content Types
247270

248271
Helidon supports two resource content types:

docs/mcp/README.md

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,6 @@ McpPromptContent image = McpPromptContents.imageContent("base64", MediaTypes.APP
221221
`Resources` allow servers to share data that provides context to language models, such as files, database schemas, or
222222
application-specific information. Clients can list and read resources, which are defined by name, description, and media type.
223223

224-
**Resource Templates** use [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) to enable dynamic discovery. They cannot
225-
be read but serve as placeholders for real resources. To define a resource or a template, use the same API. Parameters enclosed
226-
in `{}` indicate template variables.
227-
228224
#### Interface
229225

230226
Implement the `McpResource` interface and register it via `addResource`.
@@ -233,7 +229,7 @@ Implement the `McpResource` interface and register it via `addResource`.
233229
class MyResource implements McpResource {
234230
@Override
235231
public String uri() {
236-
return "http://path";
232+
return "https://path";
237233
}
238234

239235
@Override
@@ -269,7 +265,7 @@ class McpServer {
269265
.routing(routing -> routing.addFeature(
270266
McpServerFeature.builder()
271267
.addResource(resource -> resource.name("MyResource")
272-
.uri("http://path")
268+
.uri("https://path")
273269
.description("Resource description")
274270
.mediaType(MediaTypes.TEXT_PLAIN)
275271
.ressource(request -> McpResourceContents.textContent("text"))
@@ -278,6 +274,76 @@ class McpServer {
278274
}
279275
```
280276

277+
### Resource Templates
278+
279+
Resource Templates utilize [URI templates](https://datatracker.ietf.org/doc/html/rfc6570) to facilitate dynamic resource discovery.
280+
The URI template is matched against the corresponding URI in the client request. To define a resource or template, the same
281+
API as `McpResource` is employed. Parameters enclosed in `{}` denote template variables, which can be accessed via `McpParameters`
282+
using keys that correspond to these variables.
283+
284+
#### Interface
285+
286+
Implement the `McpResource` interface and register it via `addResource`.
287+
288+
```java
289+
class MyResource implements McpResource {
290+
@Override
291+
public String uri() {
292+
return "https://{path}";
293+
}
294+
295+
@Override
296+
public String name() {
297+
return "MyResource";
298+
}
299+
300+
@Override
301+
public String description() {
302+
return "Resource description";
303+
}
304+
305+
@Override
306+
public MediaType mediaType() {
307+
return MediaTypes.TEXT_PLAIN;
308+
}
309+
310+
@Override
311+
public List<McpResourceContent> read(McpRequest request) {
312+
String path = request.parameters()
313+
.get("path")
314+
.asString()
315+
.orElse("Unknown");
316+
return List.of(McpResourceContents.textContent(path));
317+
}
318+
}
319+
```
320+
321+
#### Builder
322+
323+
Define a resource in the builder using `addResource`.
324+
325+
```java
326+
class McpServer {
327+
public static void main(String[] args) {
328+
WebServer.builder()
329+
.routing(routing -> routing.addFeature(
330+
McpServerFeature.builder()
331+
.addResource(resource -> resource.name("MyResource")
332+
.uri("https://{path}")
333+
.description("Resource description")
334+
.mediaType(MediaTypes.TEXT_PLAIN)
335+
.ressource(request -> {
336+
String path = request.parameters()
337+
.get("path")
338+
.asString()
339+
.orElse("Unknown");
340+
return McpResourceContents.textContent(path);
341+
})
342+
.build())));
343+
}
344+
}
345+
```
346+
281347
#### Resource Content Types
282348

283349
Helidon supports two resource content types:

examples/calendar/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@
7474
<artifactId>junit-jupiter-api</artifactId>
7575
<scope>test</scope>
7676
</dependency>
77+
<dependency>
78+
<groupId>org.junit.jupiter</groupId>
79+
<artifactId>junit-jupiter-engine</artifactId>
80+
<scope>test</scope>
81+
</dependency>
7782
<dependency>
7883
<groupId>org.hamcrest</groupId>
7984
<artifactId>hamcrest-all</artifactId>

examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/Calendar.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.nio.file.Path;
2323
import java.nio.file.StandardOpenOption;
2424
import java.util.List;
25+
import java.util.function.Predicate;
26+
import java.util.stream.Collectors;
2527

2628
import io.helidon.extensions.mcp.server.McpException;
2729

@@ -37,7 +39,7 @@ final class Calendar {
3739
try {
3840
this.file = Files.createTempFile("calendar", "-calendar");
3941
this.uri = file.toUri().toString();
40-
this.uriTemplate = uri.substring(0, uri.lastIndexOf('-') + 1) + "{path}";
42+
this.uriTemplate = "file://events/{name}";
4143
} catch (IOException ex) {
4244
throw new UncheckedIOException(ex);
4345
}
@@ -55,7 +57,18 @@ String readContent() {
5557
try {
5658
return Files.readString(file);
5759
} catch (IOException e) {
58-
throw new RuntimeException(e);
60+
throw new UncheckedIOException(e);
61+
}
62+
}
63+
64+
String readContentMatchesLine(Predicate<String> lineMatcher) {
65+
try {
66+
return Files.readAllLines(file)
67+
.stream()
68+
.filter(lineMatcher)
69+
.collect(Collectors.joining("\n"));
70+
} catch (IOException e) {
71+
throw new UncheckedIOException(e);
5972
}
6073
}
6174

examples/calendar/src/main/java/io/helidon/extensions/mcp/examples/calendar/CalendarEventResourceTemplate.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121

2222
import io.helidon.common.media.type.MediaType;
2323
import io.helidon.common.media.type.MediaTypes;
24-
import io.helidon.extensions.mcp.server.McpException;
2524
import io.helidon.extensions.mcp.server.McpRequest;
2625
import io.helidon.extensions.mcp.server.McpResource;
2726
import io.helidon.extensions.mcp.server.McpResourceContent;
27+
import io.helidon.extensions.mcp.server.McpResourceContents;
2828

2929
/**
3030
* Resource template to help accessing to the event registry.
@@ -48,7 +48,7 @@ public String name() {
4848

4949
@Override
5050
public String description() {
51-
return "Resource Template to find calendar events registry, path is \"calendar\"";
51+
return "Resource Template to find calendar events with name";
5252
}
5353

5454
@Override
@@ -58,6 +58,13 @@ public MediaType mediaType() {
5858

5959
@Override
6060
public Function<McpRequest, List<McpResourceContent>> resource() {
61-
throw new McpException("Resource template cannot be read.");
61+
return request -> {
62+
String name = request.parameters()
63+
.get("name")
64+
.asString()
65+
.orElse("Unknown");
66+
String content = calendar.readContentMatchesLine(line -> line.startsWith("Event: { name: " + name + ","));
67+
return List.of(McpResourceContents.textContent(content));
68+
};
6269
}
6370
}

examples/calendar/src/test/java/io/helidon/extensions/mcp/examples/calendar/BaseTest.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525

2626
import io.modelcontextprotocol.client.McpSyncClient;
2727
import io.modelcontextprotocol.spec.McpSchema;
28+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
2829
import org.junit.jupiter.api.Order;
2930
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.TestMethodOrder;
3032

3133
import static org.hamcrest.MatcherAssert.assertThat;
3234
import static org.hamcrest.Matchers.containsString;
@@ -36,6 +38,7 @@
3638
import static org.hamcrest.Matchers.nullValue;
3739
import static org.hamcrest.Matchers.startsWith;
3840

41+
@TestMethodOrder(OrderAnnotation.class)
3942
abstract class BaseTest {
4043

4144
@SetUpRoute
@@ -174,10 +177,27 @@ void testResourceTemplateList() {
174177
assertThat(templates.size(), is(1));
175178

176179
McpSchema.ResourceTemplate template = templates.getFirst();
177-
assertThat(template.uriTemplate(), containsString("{path}"));
180+
assertThat(template.uriTemplate(), containsString("{name}"));
178181
assertThat(template.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
179182
assertThat(template.name(), is("calendar-events-resource-template"));
180-
assertThat(template.description(), is("Resource Template to find calendar events registry, path is \"calendar\""));
183+
assertThat(template.description(), is("Resource Template to find calendar events with name"));
184+
}
185+
186+
@Test
187+
@Order(8)
188+
void testResourceTemplateCall() {
189+
McpSchema.ReadResourceRequest request = new McpSchema.ReadResourceRequest("file://events/Franck-birthday");
190+
McpSchema.ReadResourceResult result = client().readResource(request);
191+
var contents = result.contents();
192+
assertThat(contents.size(), is(1));
193+
194+
McpSchema.ResourceContents content = contents.getFirst();
195+
assertThat(content, instanceOf(McpSchema.TextResourceContents.class));
196+
197+
McpSchema.TextResourceContents text = (McpSchema.TextResourceContents) content;
198+
assertThat(content.uri(), is("file://events/Franck-birthday"));
199+
assertThat(content.mimeType(), is(MediaTypes.TEXT_PLAIN_VALUE));
200+
assertThat(text.text(), is("Event: { name: Franck-birthday, date: 2021-04-20, attendees: [Franck] }"));
181201
}
182202

183203
private int sortArguments(McpSchema.PromptArgument first, McpSchema.PromptArgument second) {

server/src/main/java/io/helidon/extensions/mcp/server/McpJsonRpc.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ static JsonObject listTools(McpPage<McpTool> page) {
217217
return resources.build();
218218
}
219219

220-
static JsonObject listResourceTemplates(McpPage<McpResource> page) {
220+
static JsonObject listResourceTemplates(McpPage<McpResourceTemplate> page) {
221221
JsonArrayBuilder builder = JSON_BUILDER_FACTORY.createArrayBuilder();
222222
page.components().stream()
223223
.map(McpJsonRpc::resourceTemplates)

0 commit comments

Comments
 (0)