Skip to content

Commit

Permalink
Merge pull request #29 from mkouba/error-handling
Browse files Browse the repository at this point in the history
Implement error handling
  • Loading branch information
mkouba authored Dec 19, 2024
2 parents 6a14d86 + 54396e6 commit 1ac8e87
Show file tree
Hide file tree
Showing 27 changed files with 645 additions and 67 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public class ServerFeatures {

Step #3 - run your Quarkus app!

> [!NOTE]
> Currently, only the [HTTP/SSE](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse) transport is supported.

Read the full [documentation](https://quarkiverse.github.io/quarkiverse-docs/quarkus-mcp-server/dev/index.html).

## Contributors ✨
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ void registerForReflection(List<FeatureMethodBuildItem> featureMethods,
// FIXME this is not ideal, JsonObject.encode() may use Jackson under the hood which requires reflection
for (FeatureMethodBuildItem m : featureMethods) {
for (org.jboss.jandex.Type paramType : m.getMethod().parameterTypes()) {
if (paramType.kind() == Kind.PRIMITIVE) {
if (paramType.kind() == Kind.PRIMITIVE
|| paramType.name().equals(DotNames.MCP_CONNECTION)
|| paramType.name().equals(DotNames.REQUEST_ID)) {
continue;
}
reflectiveHierarchies.produce(ReflectiveHierarchyBuildItem.builder(paramType).build());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkiverse.mcp.server.test.prompts;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.Checks;
import io.quarkiverse.mcp.server.test.FooService;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkiverse.mcp.server.test.Options;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.vertx.core.json.JsonObject;

public class InvalidPromptNameTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, FooService.class, Options.class, Checks.class, MyPrompts.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("prompts/get")
.put("params", new JsonObject()
.put("name", "nonexistent")
.put("arguments", new JsonObject()));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.INVALID_PARAMS, error.getInteger("code"));
assertEquals("Invalid prompt name: nonexistent", error.getString("message"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkiverse.mcp.server.test.prompts;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.Checks;
import io.quarkiverse.mcp.server.test.FooService;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkiverse.mcp.server.test.Options;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.vertx.core.json.JsonObject;

public class MissingPromptArgumentTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, FooService.class, Options.class, Checks.class, MyPrompts.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("prompts/get")
.put("params", new JsonObject()
.put("name", "uni_bar")
.put("arguments", new JsonObject()));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.INVALID_PARAMS, error.getInteger("code"));
assertEquals("Missing required argument: val", error.getString("message"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkiverse.mcp.server.test.prompts;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.Prompt;
import io.quarkiverse.mcp.server.PromptMessage;
import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.smallrye.mutiny.Uni;
import io.vertx.core.json.JsonObject;

public class PromptInternalErrorTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, MyPrompts.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("prompts/get")
.put("params", new JsonObject()
.put("name", "uni_bar")
.put("arguments", new JsonObject().put("val", "lav")));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.INTERNAL_ERROR, error.getInteger("code"));
assertEquals("Internal error", error.getString("message"));
}

public static class MyPrompts {

@Prompt
Uni<PromptMessage> uni_bar(String val) {
return Uni.createFrom().failure(new NullPointerException());
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkiverse.mcp.server.test.resources;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.Checks;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.vertx.core.json.JsonObject;

public class InvalidResourceUriTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, Checks.class, MyResources.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("resources/read")
.put("params", new JsonObject()
.put("uri", "file:///nonexistent"));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.RESOURCE_NOT_FOUND, error.getInteger("code"));
assertEquals("Invalid resource uri: file:///nonexistent", error.getString("message"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.quarkiverse.mcp.server.test.resources;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.Resource;
import io.quarkiverse.mcp.server.ResourceResponse;
import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.vertx.core.json.JsonObject;

public class ResourceInternalErrorTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, MyResources.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("resources/read")
.put("params", new JsonObject()
.put("uri", "file:///project/alpha"));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.INTERNAL_ERROR, error.getInteger("code"));
assertEquals("Internal error", error.getString("message"));
}

public static class MyResources {

@Resource(uri = "file:///project/alpha")
ResourceResponse alpha(String uri) {
throw new NullPointerException();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkiverse.mcp.server.test.tools;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.net.URI;
import java.net.URISyntaxException;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkiverse.mcp.server.runtime.JsonRPC;
import io.quarkiverse.mcp.server.test.Checks;
import io.quarkiverse.mcp.server.test.FooService;
import io.quarkiverse.mcp.server.test.McpClient;
import io.quarkiverse.mcp.server.test.McpServerTest;
import io.quarkiverse.mcp.server.test.Options;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;
import io.vertx.core.json.JsonObject;

public class InvalidToolNameTest extends McpServerTest {

@RegisterExtension
static final QuarkusUnitTest config = defaultConfig()
.withApplicationRoot(
root -> root.addClasses(McpClient.class, FooService.class, Options.class, Checks.class, MyTools.class));

@Test
public void testError() throws URISyntaxException {
URI endpoint = initClient();

JsonObject message = newMessage("tools/call")
.put("params", new JsonObject()
.put("name", "nonexistent")
.put("arguments", new JsonObject()));

JsonObject response = new JsonObject(given()
.contentType(ContentType.JSON)
.when()
.body(message.encode())
.post(endpoint)
.then()
.statusCode(200)
.extract().body().asString());

JsonObject error = response.getJsonObject("error");
assertNotNull(error);
assertEquals(JsonRPC.INVALID_PARAMS, error.getInteger("code"));
assertEquals("Invalid tool name: nonexistent", error.getString("message"));
}

}
Loading

0 comments on commit 1ac8e87

Please sign in to comment.