Skip to content

feat(mcp): introduce Model Context Protocol (MCP) support#3886

Open
jknack wants to merge 31 commits intomainfrom
3830
Open

feat(mcp): introduce Model Context Protocol (MCP) support#3886
jknack wants to merge 31 commits intomainfrom
3830

Conversation

@jknack
Copy link
Copy Markdown
Member

@jknack jknack commented Mar 29, 2026

Description

This commit introduces first-class support for the Model Context Protocol (MCP) in Jooby, built natively on top of the official MCP Java SDK. It provides a highly efficient, annotation-driven architecture for exposing server capabilities to LLM clients.

Key Features & Architecture:

  • Annotation-Driven: Introduces a new suite of annotations in the io.jooby.annotation.mcp namespace to define server capabilities.
  • Smart Schema Documentation: The APT processor automatically extracts rich descriptions for tools and parameters by intelligently falling back from explicit annotation attributes (e.g., @McpTool(description="...")) to standard Javadoc comments, ensuring LLMs get the context they need without forcing developers to duplicate documentation.
  • Zero-Overhead Generation: The APT processor automatically generates a specialized *Mcp_ routing class that implements io.jooby.mcp.McpService, ensuring fast startup without runtime reflection.
  • Flexible JSON Ecosystems: The core McpModule is completely decoupled from JSON serialization. Developers must explicitly register either McpJackson2Module (for Jackson 2) or McpJackson3Module (for Jackson 3) alongside their respective Jooby Jackson modules.
  • Comprehensive Transports: Fully supports all major MCP communication protocols, including Stateless, Streamable HTTP, Server-Sent Events (SSE), and WebSockets.
  • Multi-Server Routing: Developers can configure and expose multiple distinct MCP servers within the same application using the @McpServer annotation.
  • Developer Tooling: Includes an out-of-the-box McpInspectorModule to facilitate easy testing and debugging of MCP endpoints during development.

Example

Input:

import java.util.List;
import java.util.Optional;

import io.jooby.annotation.mcp.McpCompletion;
import io.jooby.annotation.mcp.McpPrompt;
import io.jooby.annotation.mcp.McpResource;
import io.jooby.annotation.mcp.McpTool;
import io.modelcontextprotocol.server.McpSyncServerExchange;

/** A collection of tools, prompts, and resources exposed to the LLM via MCP. */
public class CalculatorTools {

  /**
   * Adds two integers together and returns the result.
   *
   * @param a The first number to add.
   * @param b The second number to add.
   * @return The sum of the two numbers.
   */
  @McpTool(name = "add_numbers")
  public int add(int a, int b) {
    return a + b;
  }

  @McpPrompt(name = "math_tutor", description = "A prompt to initiate a math tutoring session")
  public String mathTutor(String topic) {
    return "You are a helpful math tutor. Please explain the concept of "
        + topic
        + " step by step.";
  }

  @McpResource(
      uri = "calculator://manual/usage",
      name = "Calculator Manual",
      description = "Instructions on how to use the calculator")
  public String manual() {
    return "The Calculator supports basic arithmetic. Use the add_numbers tool to sum values.";
  }

  @McpResource(
      uri = "calculator://history/{user}",
      name = "User History",
      description = "Retrieves the calculation history for a specific user")
  public String history(String user) {
    return "History for " + user + ":\n5 + 10 = 15\n2 * 4 = 8";
  }

  @McpCompletion(ref = "calculator://history/{user}")
  public List<String> historyCompletions(String user) {
    // In a real app, this would query a database for active usernames matching the input
    return List.of("alice", "bob", "charlie");
  }

  @McpTool(
      name = "get_session_info",
      description = "Returns the current MCP session ID using the injected exchange.")
  public String getSessionInfo(McpSyncServerExchange exchange) {
    return Optional.ofNullable(exchange.sessionId()).orElse("No active session");
  }
}

Output:

@io.jooby.annotation.Generated(CalculatorTools.class)
public class CalculatorToolsMcp_ implements io.jooby.mcp.McpService {
    protected java.util.function.Function<io.jooby.Context, CalculatorTools> factory;

    public CalculatorToolsMcp_() {
      this(io.jooby.SneakyThrows.singleton(CalculatorTools::new));
    }

    public CalculatorToolsMcp_(CalculatorTools instance) {
      setup(ctx -> instance);
    }

    public CalculatorToolsMcp_(io.jooby.SneakyThrows.Supplier<CalculatorTools> provider) {
      setup(ctx -> provider.get());
    }

    public CalculatorToolsMcp_(io.jooby.SneakyThrows.Function<Class<CalculatorTools>, CalculatorTools> provider) {
      setup(ctx -> provider.apply(CalculatorTools.class));
    }

    private void setup(java.util.function.Function<io.jooby.Context, CalculatorTools> factory) {
      this.factory = factory;
    }

    private io.modelcontextprotocol.json.McpJsonMapper json;
    @Override
    public void capabilities(io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.Builder capabilities) {
      capabilities.tools(true);
      capabilities.prompts(true);
      capabilities.resources(true, true);
      capabilities.completions();
    }

    @Override
    public String serverKey() {
      return "default";
    }

    @Override
    public java.util.List<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification> completions() {
      var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification>();
      completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("math_tutor"), (exchange, req) -> new io.jooby.mcp.McpResult(this.json).toCompleteResult(java.util.List.of())));
      completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("calculator://history/{user}"), (exchange, req) -> this.historyCompletionHandler(exchange, null, req)));
      return completions;
    }

    @Override
    public java.util.List<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification> statelessCompletions() {
      var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification>();
      completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("math_tutor"), (ctx, req) -> new io.jooby.mcp.McpResult(this.json).toCompleteResult(java.util.List.of())));
      completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("calculator://history/{user}"), (ctx, req) -> this.historyCompletionHandler(null, ctx, req)));
      return completions;
    }

    @Override
    public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpSyncServer server) throws Exception {
      this.json = app.require(io.modelcontextprotocol.json.McpJsonMapper.class);
      var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);

      server.addTool(new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (exchange, req) -> this.add(exchange, null, req)));
      server.addPrompt(new io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification(mathTutorPromptSpec(), (exchange, req) -> this.mathTutor(exchange, null, req)));
      server.addResource(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification(manualResourceSpec(), (exchange, req) -> this.manual(exchange, null, req)));
      server.addResourceTemplate(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification(historyResourceTemplateSpec(), (exchange, req) -> this.history(exchange, null, req)));
      server.addTool(new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(getSessionInfoToolSpec(schemaGenerator), (exchange, req) -> this.getSessionInfo(exchange, null, req)));
    }

    @Override
    public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpStatelessSyncServer server) throws Exception {
      this.json = app.require(io.modelcontextprotocol.json.McpJsonMapper.class);
      var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);

      server.addTool(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (ctx, req) -> this.add(null, ctx, req)));
      server.addPrompt(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification(mathTutorPromptSpec(), (ctx, req) -> this.mathTutor(null, ctx, req)));
      server.addResource(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification(manualResourceSpec(), (ctx, req) -> this.manual(null, ctx, req)));
      server.addResourceTemplate(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification(historyResourceTemplateSpec(), (ctx, req) -> this.history(null, ctx, req)));
      server.addTool(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(getSessionInfoToolSpec(schemaGenerator), (ctx, req) -> this.getSessionInfo(null, ctx, req)));
    }

    private io.modelcontextprotocol.spec.McpSchema.Tool addToolSpec(com.github.victools.jsonschema.generator.SchemaGenerator schemaGenerator) {
      var schema = new java.util.LinkedHashMap<String, Object>();
      schema.put("type", "object");
      var props = new java.util.LinkedHashMap<String, Object>();
      schema.put("properties", props);
      var req = new java.util.ArrayList<String>();
      schema.put("required", req);
      var schema_a = schemaGenerator.generateSchema(int.class);
      props.put("a", schema_a);
      req.add("a");
      var schema_b = schemaGenerator.generateSchema(int.class);
      props.put("b", schema_b);
      req.add("b");
      return new io.modelcontextprotocol.spec.McpSchema.Tool("add_numbers", null, null, this.json.convertValue(schema, io.modelcontextprotocol.spec.McpSchema.JsonSchema.class), null, null, null);
    }

    private io.modelcontextprotocol.spec.McpSchema.CallToolResult add(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.CallToolRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var args = req.arguments() != null ? req.arguments() : java.util.Collections.<String, Object>emptyMap();
      var c = this.factory.apply(ctx);
      var raw_a = args.get("a");
      if (raw_a == null) throw new IllegalArgumentException("Missing req param: a");
      var a = ((Number) raw_a).intValue();
      var raw_b = args.get("b");
      if (raw_b == null) throw new IllegalArgumentException("Missing req param: b");
      var b = ((Number) raw_b).intValue();
      var result = c.add(a, b);
      return new io.jooby.mcp.McpResult(this.json).toCallToolResult(result, false);
    }

    private io.modelcontextprotocol.spec.McpSchema.Prompt mathTutorPromptSpec() {
      var args = new java.util.ArrayList<io.modelcontextprotocol.spec.McpSchema.PromptArgument>();
      args.add(new io.modelcontextprotocol.spec.McpSchema.PromptArgument("topic", null, false));
      return new io.modelcontextprotocol.spec.McpSchema.Prompt("math_tutor", null, "A prompt to initiate a math tutoring session", args);
    }

    private io.modelcontextprotocol.spec.McpSchema.GetPromptResult mathTutor(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.GetPromptRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var args = req.arguments() != null ? req.arguments() : java.util.Collections.<String, Object>emptyMap();
      var c = this.factory.apply(ctx);
      var raw_topic = args.get("topic");
      var topic = raw_topic != null ? raw_topic.toString() : null;
      var result = c.mathTutor(topic);
      return new io.jooby.mcp.McpResult(this.json).toPromptResult(result);
    }

    private io.modelcontextprotocol.spec.McpSchema.Resource manualResourceSpec() {
      return new io.modelcontextprotocol.spec.McpSchema.Resource("calculator://manual/usage", "Calculator Manual", null, "Instructions on how to use the calculator", io.jooby.MediaType.byFileExtension("calculator://manual/usage", "text/plain").getValue(), null, null, null);
    }

    private io.modelcontextprotocol.spec.McpSchema.ReadResourceResult manual(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var args = java.util.Collections.<String, Object>emptyMap();
      var c = this.factory.apply(ctx);
      var result = c.manual();
      return new io.jooby.mcp.McpResult(this.json).toResourceResult(req.uri(), result);
    }

    private io.modelcontextprotocol.spec.McpSchema.ResourceTemplate historyResourceTemplateSpec() {
      return new io.modelcontextprotocol.spec.McpSchema.ResourceTemplate("calculator://history/{user}", "User History", null, "Retrieves the calculation history for a specific user", io.jooby.MediaType.byFileExtension("calculator://history/{user}", "text/plain").getValue(), null, null);
    }

    private io.modelcontextprotocol.spec.McpSchema.ReadResourceResult history(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var uri = req.uri();
      var manager = new io.modelcontextprotocol.util.DefaultMcpUriTemplateManager("calculator://history/{user}");
      var args = new java.util.HashMap<String, Object>();
      args.putAll(manager.extractVariableValues(uri));
      var c = this.factory.apply(ctx);
      var raw_user = args.get("user");
      var user = raw_user != null ? raw_user.toString() : null;
      var result = c.history(user);
      return new io.jooby.mcp.McpResult(this.json).toResourceResult(req.uri(), result);
    }

    private io.modelcontextprotocol.spec.McpSchema.Tool getSessionInfoToolSpec(com.github.victools.jsonschema.generator.SchemaGenerator schemaGenerator) {
      var schema = new java.util.LinkedHashMap<String, Object>();
      schema.put("type", "object");
      var props = new java.util.LinkedHashMap<String, Object>();
      schema.put("properties", props);
      var req = new java.util.ArrayList<String>();
      schema.put("required", req);
      return new io.modelcontextprotocol.spec.McpSchema.Tool("get_session_info", null, "Returns the current MCP session ID using the injected exchange.", this.json.convertValue(schema, io.modelcontextprotocol.spec.McpSchema.JsonSchema.class), null, null, null);
    }

    private io.modelcontextprotocol.spec.McpSchema.CallToolResult getSessionInfo(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.CallToolRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var args = req.arguments() != null ? req.arguments() : java.util.Collections.<String, Object>emptyMap();
      var c = this.factory.apply(ctx);
      var result = c.getSessionInfo(exchange);
      return new io.jooby.mcp.McpResult(this.json).toCallToolResult(result, false);
    }

    private io.modelcontextprotocol.spec.McpSchema.CompleteResult historyCompletionHandler(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.common.McpTransportContext transportContext, io.modelcontextprotocol.spec.McpSchema.CompleteRequest req) {
      var ctx = exchange != null ? (io.jooby.Context) exchange.transportContext().get("CTX") : (transportContext != null ? (io.jooby.Context) transportContext.get("CTX") : null);
      var c = this.factory.apply(ctx);
      var targetArg = req.argument() != null ? req.argument().name() : "";
      var typedValue = req.argument() != null ? req.argument().value() : "";
      return switch (targetArg) {
        case "user" -> {
          var result = c.historyCompletions(typedValue);
          yield new io.jooby.mcp.McpResult(this.json).toCompleteResult(result);
        }
        default -> new io.jooby.mcp.McpResult(this.json).toCompleteResult(java.util.List.of());
      };
    }
}

jknack added 29 commits March 21, 2026 11:54
Introduces compile-time annotation processing for the Model Context Protocol (MCP), generating highly optimized, reflection-free dispatchers that bridge untyped JSON-RPC arguments to strongly-typed Jooby controllers.

Key implementations:
- Add `@McpServer`, `@McpTool`, `@McpPrompt`, `@McpResource`, and `@McpCompletion` annotations for defining MCP capabilities.
- Add `@McpParam` to customize schema parameter names and provide LLM descriptions (with planned Javadoc fallback).
- Update `MvcRoute` to classify MCP components during construction and isolate them into a dedicated `mcpRoutes` list in `MvcRouter` to prevent REST generator conflicts.
- Define `McpService` interface enforcing a strict execution contract with `McpSyncServerExchange` for Context resolution.
- Implement `getMcpSourceCode` in `MvcRouter` to generate `*McpServer_` classes featuring:
  - Zero-reflection `switch/case` routing for tools, prompts, resources, templates, and completions.
  - Safe extraction and primitive casting from untyped argument maps.
  - Strict Jackson 3 (`tools.jackson`) registry lookup for complex POJO conversions.
  - Native Jooby dependency injection support using the existing `constructors()` AST utility.
  - Interface compliance by throwing `UnsupportedOperationException` for unused MCP capabilities.
- normalize: constructor generation
Implement full Annotation Processor (APT) support for generating MCP
server specifications and handlers, bridging Jooby controllers with the
Model Context Protocol SDK.

Key implementations:
* Annotations: Add code generation for @mcptool, @McpPrompt,
  @McpResource, and @McpCompletion.
* Completion Routing: Implement "Group and Route" architecture. Groups
  multiple argument-specific completion methods under a single MCP
  Reference and routes them at runtime using compile-time generated
  enhanced switch/when expressions.
* Dynamic Resources: Auto-detect URI placeholders (`{}`) to dynamically
  register as Resource Templates or static Resources.
* Template Variables: Integrate `DefaultMcpUriTemplateManager` to safely
  extract and bind path variables from `req.uri()` into controller args.
* Schema Generation: Optimize Jackson 3 (`tools.jackson`) schema builder
  to automatically omit output schemas for primitives, standard java.lang
  types, and internal MCP classes.
* Idiomatic Output: Generate clean, modern Java (var, enhanced switch)
  and Kotlin (val, when) with strict null-safety checks and safe default
  fallbacks for unmatched requests.
- into: rest, jsonrpc, trpc and mcp
- get back tests passed
This commit fixes an issue where `@McpCompletion` requests were failing
with a "-32601 Missing handler" error because the `completions()` capability
was omitted from the generated `ServerCapabilities`. It also introduces a
major refactor to the APT generation to eliminate duplicate code.

Details:
* Added `capabilities.completions()` to the generated `capabilities()` method.
* Safely stripped lingering AST quotes from `@McpCompletion` annotation
  values to ensure precise router matching.
* Refactored `McpRoute` to generate a single, unified handler method accepting
  both `McpSyncServerExchange` and `McpTransportContext` alongside the request,
  dynamically extracting `io.jooby.Context` from the available transport.
* Updated `McpRouter` to register tools, prompts, resources, and completions
  using clean lambda adapters that map the current server type (stateful vs.
  stateless) to the unified handler.
- add web-socket transport
This commit refactors the transport layer by removing the redundant
"Jooby" prefix and "Server" identifiers from the class names, as their
context is already clear within the `io.jooby.mcp.transport` package.

To provide a consistent and predictable API, all transport implementations
now share the `TransportProvider` suffix.

Renames:
* JoobySseTransportProvider -> SseTransportProvider
* JoobyStatelessServerTransport -> StatelessTransportProvider
* JoobyStreamableServerTransportProvider -> StreamableTransportProvider
* JoobyWebSocketServerTransportProvider -> WebSocketTransportProvider
…e transport

This commit fixes a race condition where the Streamable HTTP transport
would randomly return an empty payload ("") instead of the tool execution
result. The failure occurred because the Jooby I/O thread was being blocked,
causing the server to tear down the TCP socket before the final SSE chunk
could be flushed to the network.

Details:
* Replaced `.block()` calls inside `ctx.upgrade()` with non-blocking
  Reactor `.subscribe()` chains to prevent I/O thread deadlocks.
* Refactored `notifyClients`, `closeGracefully`, and the `lastId` replay
  loop to use native Reactor `Flux` chains instead of blocking Java
  `.parallelStream().forEach()` loops.
* Added a 50ms `Mono.delay` to `JoobyStreamableMcpSessionTransport.closeGracefully()`.
  This guarantees the underlying server (e.g., Undertow) has a sufficient
  buffer window to physically flush the final SSE event to the network
  layer before the connection is destroyed.
This commit significantly expands the `@McpResource` annotation to fully
support the Model Context Protocol (MCP) Resource specification, and updates
the annotation processor to map these new fields into the SDK constructors.

Details:
* Renamed the default `value` attribute to `uri` for better clarity.
* Added support for `title`, `description`, `mimeType`, and `size` metadata.
* Introduced a nested `@McpAnnotations` interface to support advanced
  resource metadata, including `audience` roles, `priority`, and
  `lastModified` dates.
* Updated the `McpRoute` APT generator to extract the new attributes and
  map them correctly into `McpSchema.Resource` and `McpSchema.ResourceTemplate`.
* Implemented string-based parsing for nested annotations in `McpRoute` to
  safely generate the `ResourceAnnotations` object for Java and Kotlin, bypassing
  the need for heavy `ElementVisitor` boilerplate.
This commit expands the `@McpTool` annotation to fully support the latest
Model Context Protocol (MCP) tool specification, allowing developers to
provide richer metadata to the LLM.

Details:
* Added support for the `title` attribute in `@McpTool` for human-readable
  display names.
* Introduced nested `@McpAnnotations` for tools to support execution
  hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and
  `openWorldHint`.
* Updated the `McpRoute` APT generator to parse the nested tool annotations
  from their string representation, falling back to spec defaults when omitted.
* Fixed compilation errors in the code generator by correctly aligning the
  constructor arguments for `McpSchema.ToolAnnotations`.
This commit completely decouples the MCP core and APT code generator
from Jackson and victools, resolving classpath conflicts between Jackson 2
and Jackson 3. It introduces dedicated integration modules to cleanly
handle JSON serialization and JSON Schema generation based on the user's
runtime environment.

Details:
* Refactored the APT generator (`McpRoute`, `McpRouter`) to build schemas
  using standard `java.util.LinkedHashMap` and `java.util.ArrayList`
  instead of Jackson's `ObjectNode` and `ArrayNode`.
* Removed hardcoded `victools` configuration from the generated `install`
  methods. The generated router now dynamically requires the `SchemaGenerator`
  from the Jooby application registry.
* Delegated the final schema map conversion strictly to the abstracted
  `McpJsonMapper` interface.
* Introduced the `jooby-mcp-jackson2` module to provide bindings for
  Jackson 2 and `victools` 4.x.
* Introduced the `jooby-mcp-jackson3` module to provide bindings for
  Jackson 3 and `victools` 5.x.
- move transport as internal package
- add mcp-inspector
- add automatic-module-name
- fix error for missing completions
@jknack
Copy link
Copy Markdown
Member Author

jknack commented Mar 29, 2026

/cc @kliushnichenko

jknack added 2 commits March 29, 2026 19:50
Since the routing lambdas now directly inject the transport context
for both stateful and stateless flows, the `transportContext` parameter
is guaranteed to be non-null in the generated handler methods.

This commit cleans up the APT generator (`McpRoute`, `McpRouter`) by:
* Removing redundant null-checks and ternary operators associated with
  the transport context.
* Eliminating Kotlin safe-calls (`?.`) in method signatures and
  variable extractions.
* Streamlining Jooby `Context` extraction to use direct, safe casts
  (e.g., `(io.jooby.Context) transportContext.get("CTX")`).
* Significantly reducing the boilerplate and branching in all generated
  tool, prompt, resource, and completion handlers.
MCP tool, prompt, resource, and completion calls. It centralizes
execution logic, exception handling, and protocol error mapping, while
providing developers a clean hook to inject custom telemetry, tracing,
or MDC context propagation.

Details:
* Added `McpInvoker` interface and `DefaultMcpInvoker` implementation.
* Integrated Jooby's `Router.errorCode()` to seamlessly map standard
  framework exceptions (e.g., 400 Bad Request) to standard MCP
  JSON-RPC errors (e.g., -32602 Invalid Params).
* Implemented LLM "self-healing" for tools: Unhandled business exceptions
  are now caught and returned as a `CallToolResult` with `isError=true`.
  This prevents protocol aborts and feeds the error text directly back
  into the LLM context so it can self-correct.
* Updated the APT generator (`McpRouter`) to dynamically resolve the
  `McpInvoker` from the Jooby application registry using local variables,
  ensuring the generated router remains completely stateless.
* Wrapped all routing lambdas in `invoker.invoke(operationId, action)`,
  passing contextual operation IDs (e.g., `tools/add_numbers` or
  `resources/calculator://history/{user}`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant