Skip to content

Conversation

@patcher454
Copy link

Related:

Motivation:

Modifications:

  • Add Implement JsonRpcService

Result:

@ikhoon ikhoon changed the base branch from main to json-rpc June 24, 2025 02:50
@patcher454 patcher454 marked this pull request as ready for review July 3, 2025 09:13
@patcher454
Copy link
Author

If you have a moment, I would appreciate a review.

/**
* Returns the JSON-RPC version.
*/
String version();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we define the version values as an enum type?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a JsonRpcVersion enum.
Could you please check if this aligns with what you intended?

/**
* Returns the parameters for the JSON-RPC method.
*/
List<Object> params();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List<Object> would not express by-name parameters. We may introduce JsonRpcParameter to abstract by-position and by-name.

https://www.jsonrpc.org/specification#parameter_structures
by-name: params MUST be an Object,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented the suggestion to introduce JsonRpcParameter to abstract both by-position and by-name parameters.
Please review the changes.

Comment on lines 168 to 174
// Notification
if (req.id() == null) {
if (handler != null) {
handler.handle(ctx, req);
}
return UnmodifiableFuture.completedFuture(null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we return the future when handler.handle(ctx, req) completes.
In addition, it would be worth returning the status code defined in the MCP specification for compatibility.

  • Should we return 202 Accepted instead if the handler accepts the notification?
  • Should we return 400 Bad Request if the handler raises an exception?

If the input is a JSON-RPC response or notification:

  • If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body.
  • If the server cannot accept the input, it MUST return an HTTP error status code (e.g., 400 Bad Request). The HTTP response body MAY comprise a JSON-RPC error response that has no id.

https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#:~:text=if%20the%20server%20accepts%20the%20input%2C%20the%20server%20must%20return%20http%20status%20code%20202%20accepted%20with%20no%20body.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we return the future when handler.handle(ctx, req) completes.

I don't think this comment is addressed. My suggestion was:

if (handler != null) {
    return handler.handle(ctx, req)
                  .thenApply(res -> null);
}
return UnmodifiableFuture.completedFuture(null);

@patcher454 patcher454 requested a review from ikhoon October 23, 2025 14:33
@codecov
Copy link

codecov bot commented Oct 24, 2025

Codecov Report

❌ Patch coverage is 69.91870% with 74 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (json-rpc@bd3e002). Learn more about missing BASE report.

Files with missing lines Patch % Lines
.../armeria/common/jsonrpc/DefaultJsonRpcRequest.java 63.46% 16 Missing and 3 partials ⚠️
...inecorp/armeria/server/jsonrpc/JsonRpcService.java 81.42% 6 Missing and 7 partials ⚠️
.../linecorp/armeria/common/jsonrpc/JsonRpcError.java 61.29% 7 Missing and 5 partials ⚠️
...armeria/server/jsonrpc/DefaultJsonRpcResponse.java 42.85% 7 Missing and 5 partials ⚠️
.../armeria/common/jsonrpc/SimpleJsonRpcResponse.java 26.66% 11 Missing ⚠️
...ecorp/armeria/common/jsonrpc/JsonRpcParameter.java 57.14% 2 Missing and 4 partials ⚠️
...inecorp/armeria/common/jsonrpc/JsonRpcRequest.java 92.85% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             json-rpc    #6289   +/-   ##
===========================================
  Coverage            ?   74.11%           
  Complexity          ?    23087           
===========================================
  Files               ?     2073           
  Lines               ?    86374           
  Branches            ?    11341           
===========================================
  Hits                ?    64012           
  Misses              ?    16930           
  Partials            ?     5432           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

import com.linecorp.armeria.common.jsonrpc.JsonRpcError;
import com.linecorp.armeria.common.jsonrpc.JsonRpcVersion;

@UnstableApi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@UnstableApi is used in public and protected API.

Suggested change
@UnstableApi

static JsonRpcRequest of(JsonNode node) throws JsonProcessingException {
requireNonNull(node, "node");
checkArgument(node.isObject(), "node.isObject(): %s (expected: true)", node.isObject());
return JacksonUtil.newDefaultObjectMapper().treeToValue(node, DefaultJsonRpcRequest.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's declare JacksonUtil.newDefaultObjectMapper() in DefaultJsonRpcRequest as a static value to avoid additional allocations.

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

@UnstableApi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Suggested change
@UnstableApi

Comment on lines 75 to 77
.thenApply(JsonRpcService::parseRequestContentAsJson)
.thenApply(json -> dispatchRequest(ctx, json))
.exceptionally(e -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro optimization) Should we consolidate the three callbacks into CompletableFuture.handle() method?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've refactored the code to consolidate the three callbacks into a single CompletableFuture.handle() method as you recommended.

Could you please take another look to see if this implementation aligns with what you had in mind?

if (rawRequest.isObject()) {
return handleUnaryRequest(ctx, rawRequest);
} else {
return HttpResponse.ofJson(HttpStatus.BAD_REQUEST, JsonRpcError.PARSE_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, should we return the following message if the request is an array?

JsonRpcError.INVALID_REQUEST.withData("Batch requests are not supported by this server.");


private HttpResponse toHttpResponse(DefaultJsonRpcResponse response) {
if (response == null) {
return HttpResponse.of(HttpStatus.ACCEPTED, MediaType.PLAIN_TEXT_UTF_8, "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not need a content-type and an empty string for accepted status.

Suggested change
return HttpResponse.of(HttpStatus.ACCEPTED, MediaType.PLAIN_TEXT_UTF_8, "");
return HttpResponse.of(HttpStatus.ACCEPTED);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the empty string because the MCP documentation specifies that the response for this case should have no body. If we simply return HttpResponse.of(HttpStatus.ACCEPTED), the response body will contain the string "202 Accepted".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. We could make the response body empty by specifying response headers only.

HttpResponse.of(ResponseHeaders.of(HttpStatus.ACCEPTED));

Comment on lines 168 to 174
// Notification
if (req.id() == null) {
if (handler != null) {
handler.handle(ctx, req);
}
return UnmodifiableFuture.completedFuture(null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we return the future when handler.handle(ctx, req) completes.

I don't think this comment is addressed. My suggestion was:

if (handler != null) {
    return handler.handle(ctx, req)
                  .thenApply(res -> null);
}
return UnmodifiableFuture.completedFuture(null);

*/
@UnstableApi
public class JsonRpcServiceBuilder {
private final Map<String, JsonRpcHandler> methodHandlers = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer immutable collections. Should we use ImmutableMap.Builder to collect the handlers?

@patcher454 patcher454 requested a review from ikhoon October 24, 2025 06:37
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good.

/**
* Creates a new instance with result.
*/
public AbstractJsonRpcResponse(Object result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public AbstractJsonRpcResponse(Object result) {
protected AbstractJsonRpcResponse(Object result) {

/**
* Creates a new instance with error.
*/
public AbstractJsonRpcResponse(JsonRpcError error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public AbstractJsonRpcResponse(JsonRpcError error) {
protected AbstractJsonRpcResponse(JsonRpcError error) {

final JsonRpcRequest that = (JsonRpcRequest) obj;
return Objects.equals(id, that.id()) &&
Objects.equals(method, that.method()) &&
Objects.equals(params, that.params());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Is JsonRpcVersion excluded intentionally from equals and hashCode?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally omitted it because the version was a constant,
Now that it's an enum, it definitely belongs in the equals() and hashCode() methods.

I've pushed a fix to include JsonRpcVersion in both equals() and hashCode(). Thanks

Comment on lines 52 to 59
* Returns {@code true} if this Parameter is a Positional.
*/
public boolean isPositional() {
return value instanceof List;
}

/**
* Returns {@code true} if this Parameter is a Named.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Returns {@code true} if this Parameter is a Positional.
*/
public boolean isPositional() {
return value instanceof List;
}
/**
* Returns {@code true} if this Parameter is a Named.
* Returns {@code true} if this parameter is a positional.
*/
public boolean isPositional() {
return value instanceof List;
}
/**
* Returns {@code true} if this parameter is a pamed.

Comment on lines 44 to 49
public static JsonRpcParameter of(Object parsedParams) {
checkArgument(parsedParams instanceof List || parsedParams instanceof Map,
"params type: %s (expected: List or Map)",
parsedParams != null ? parsedParams.getClass().getName() : "null");
return new JsonRpcParameter(parsedParams);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add overload methods instead of Object type for type-safety?

public static JsonRpcParameter of(List<Object> ...) { }

public static JsonRpcParameter of(Map<String, Object> ...) { }


@VisibleForTesting
CompletableFuture<DefaultJsonRpcResponse> invokeMethod(ServiceRequestContext ctx,
JsonRpcRequest req) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indent?

@patcher454 patcher454 requested a review from ikhoon October 27, 2025 05:57
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left only minor comments. 😉

Comment on lines 1718 to 1720
public static boolean jsonRpcServiceContentLogging() {
return JSON_RPC_SERVICE_CONTENT_LOGGING;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see we need this flag at the moment since annotatedServiceContentLogging was added to preserve the original behavior. Let's consider adding this option when there is a request for it.

Comment on lines 71 to 72
"id type: %s (expected: Null or Number or String)",
Optional.ofNullable(id).map(Object::getClass).orElse(null));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Optional might be a bit too much for this case. The intention will be fully conveyed by the id itself. If you prefer type, let's use a ternary operator.

Suggested change
"id type: %s (expected: Null or Number or String)",
Optional.ofNullable(id).map(Object::getClass).orElse(null));
"id: %s (expected: Null or Number or String)", id);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using id directly as suggested "id: %s (expected: Null or Number or String)", id) triggers a nullable warning.
As you also suggested, I've updated it to use a ternary operator instead. Thanks!

static JsonRpcRequest of(@Nullable Object id, String method, Map<String, Object> parameter) {
return new DefaultJsonRpcRequest(id,
requireNonNull(method, "method"),
requireNonNull(parameter, "param"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
requireNonNull(parameter, "param"));
requireNonNull(parameter, "parameter"));


/**
* Returns the ID of the JSON-RPC request.
* type must be Number or String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* type must be Number or String
* The type must be {@link Number} or {@link String}.

import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A Json-RPC.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* A Json-RPC.
* A JSON-RPC response.

import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* A Json-RPC request.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* A Json-RPC request.
* A JSON-RPC request.

* Constructs a new {@link JsonRpcService}.
*/
public JsonRpcService build() {
return new JsonRpcService(methodHandlers.build());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about raising an exception if methodHandlers.isEmpty() == true?

@patcher454 patcher454 requested a review from ikhoon October 30, 2025 17:37
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your hard work, @patcher454! 🚀👍

@patcher454 patcher454 requested a review from ikhoon October 31, 2025 08:29
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still LGTM

@patcher454
Copy link
Author

@ikhoon

It looks like the :examples:athenz-example:test task is consistently failing in the build.
Is this a known issue, or is there something specific I need to fix or modify in my changes?
Any guidance would be appreciated. Thanks!

@ikhoon
Copy link
Contributor

ikhoon commented Oct 31, 2025

#6473 Please merge main branch to resolve it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement JsonRpcService

3 participants