From 7d20791da91bc2d6dbb0dc4449493b5c444c0c46 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 16 Mar 2026 20:26:38 +0200 Subject: [PATCH 1/4] documentation powered by hugo --- .github/workflows/hugo.yml | 59 ++++++++++++ .gitignore | 4 + docs/README.md | 58 ++++++++++++ docs/assets/css/custom.css | 43 +++++++++ docs/content/_index.md | 66 +++++++++++++ docs/content/appendix-return-types.md | 41 ++++++++ docs/content/customizing-server.md | 47 ++++++++++ docs/content/exchange-object.md | 26 ++++++ docs/content/mcp-inspector.md | 49 ++++++++++ docs/content/multiple-servers.md | 55 +++++++++++ docs/content/prompts.md | 72 ++++++++++++++ docs/content/quick-start.md | 104 +++++++++++++++++++++ docs/content/resources.md | 78 ++++++++++++++++ docs/content/tools.md | 76 +++++++++++++++ docs/go.mod | 5 + docs/go.sum | 2 + docs/hugo.yaml | 62 ++++++++++++ docs/layouts/index.html | 25 +++++ docs/layouts/partials/custom/head-end.html | 18 ++++ docs/layouts/robots.txt | 4 + docs/static/css/chroma-github.css | 74 +++++++++++++++ docs/static/css/chroma-tokyonight-moon.css | 73 +++++++++++++++ docs/static/favicon.svg | 11 +++ docs/static/images/dark_mcp_logo.svg | 5 + docs/static/images/mcp_logo.svg | 12 +++ 25 files changed, 1069 insertions(+) create mode 100644 .github/workflows/hugo.yml create mode 100644 docs/README.md create mode 100644 docs/assets/css/custom.css create mode 100644 docs/content/_index.md create mode 100644 docs/content/appendix-return-types.md create mode 100644 docs/content/customizing-server.md create mode 100644 docs/content/exchange-object.md create mode 100644 docs/content/mcp-inspector.md create mode 100644 docs/content/multiple-servers.md create mode 100644 docs/content/prompts.md create mode 100644 docs/content/quick-start.md create mode 100644 docs/content/resources.md create mode 100644 docs/content/tools.md create mode 100644 docs/go.mod create mode 100644 docs/go.sum create mode 100644 docs/hugo.yaml create mode 100644 docs/layouts/index.html create mode 100644 docs/layouts/partials/custom/head-end.html create mode 100644 docs/layouts/robots.txt create mode 100644 docs/static/css/chroma-github.css create mode 100644 docs/static/css/chroma-tokyonight-moon.css create mode 100644 docs/static/favicon.svg create mode 100644 docs/static/images/dark_mcp_logo.svg create mode 100644 docs/static/images/mcp_logo.svg diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml new file mode 100644 index 0000000..fd0683e --- /dev/null +++ b/.github/workflows/hugo.yml @@ -0,0 +1,59 @@ +name: Deploy documentation to GitHub Pages + +on: + push: + branches: ["main", "feat/docs"] + paths: + - "docs/**" + - ".github/workflows/hugo.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v3 + with: + hugo-version: "latest" + hugo-extended: true + + - name: Get Hugo modules + working-directory: docs + run: hugo mod get -u + + - name: Build site + working-directory: docs + env: + # GA ID from repo secret GA_MEASUREMENT_ID (never commit the ID) + HUGO_SERVICES_GOOGLEANALYTICS_ID: ${{ secrets.GA_MEASUREMENT_ID }} + run: hugo --minify --environment production + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/public + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 524f096..c2c0199 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Hugo +docs/public/ +docs/resources/_gen/ \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..57d25c8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,58 @@ +# jooby-mcp Documentation Site + +This directory contains the source for the [jooby-mcp](https://github.com/kliushnichenko/jooby-mcp) documentation site, built with [Hugo](https://gohugo.io/) and the [Hextra](https://themes.gohugo.io/themes/hextra/) theme. + +## Prerequisites + +- [Hugo Extended](https://gohugo.io/installation/) (v0.146.0 or later) +- [Go](https://go.dev/dl/) (required for Hugo Modules) + +## Local development + +**First time** (or after adding/updating the theme): fetch the Hextra theme via Hugo Modules: + +```bash +cd docs +hugo mod get -u +``` + +Commit `go.mod` and `go.sum` so CI and others use the same theme version. + +Then run the site: + +```bash +hugo server --baseURL=http://localhost:1313/ +``` + +Open **http://localhost:1313/** in your browser. Using `--baseURL` overrides the config (which targets GitHub Pages at `.../jooby-mcp/`) so the site is served at the root locally. + +## Build for production + +```bash +cd docs +hugo --minify +``` + +Output is in `docs/public/`. + +## Syntax highlighting + +The code block colors follow Hextra’s default Chroma styles (light/dark). To use another style (e.g. `monokai`, `nord`): + +1. Generate a stylesheet (from the `docs` directory): + ```bash + hugo gen chromastyles --style=monokai > assets/css/chroma-monokai.css + ``` +2. In `assets/css/custom.css`, add: + ```css + @import "chroma-monokai.css"; + ``` +3. Rebuild. For dark mode you can generate a dark style (e.g. `--style=nord`) and scope it under `.dark` if needed. + +[Chroma style gallery](https://xyproto.github.io/splash/docs/all.html) lists all style names. + +## Deployment + +The site is deployed to [GitHub Pages](https://pages.github.com/) via the [Deploy documentation to GitHub Pages](.github/workflows/hugo.yml) workflow. Pushing changes under `docs/` to the `main` branch triggers a build and deploy. + +To enable the site: in the repository **Settings → Pages**, set **Source** to **GitHub Actions**. diff --git a/docs/assets/css/custom.css b/docs/assets/css/custom.css new file mode 100644 index 0000000..ac7d275 --- /dev/null +++ b/docs/assets/css/custom.css @@ -0,0 +1,43 @@ +/* Custom CSS for the docs site. Hextra loads this file automatically. */ + +/* + * Syntax highlight theme (Chroma) + * Hextra does not expose a config param for the highlight style; you override via CSS. + * + * To use a different style (e.g. monokai, nord, solarized-dark): + * 1. Run: hugo gen chromastyles --style=STYLE > chroma-STYLE.css + * 2. Save that file here (e.g. assets/css/chroma-monokai.css) + * 3. Uncomment and adapt the import below, or paste the generated rules here. + * + * Style names: github, monokai, nord, solarized-dark, solarized-light, dracula, etc. + * Gallery: https://xyproto.github.io/splash/docs/all.html + */ +/* Chroma (syntax highlight): light mode */ +@import "/css/chroma-github.css"; +/* Dark mode: Tokyo Night Moon */ +@import "/css/chroma-tokyonight-moon.css"; + +/* Wider layout: page (sidebar+content), content column, and navbar. Defaults: 80rem. */ +:root { + --hextra-max-page-width: 96rem; /* whole row (sidebar + content) */ + --hextra-max-content-width: 80rem; /* main text/code column */ + --hextra-max-navbar-width: 96rem; /* align navbar with page width */ +} +/* Force widths in case variables are overridden by load order */ +.hextra-max-page-width { + max-width: 96rem; +} +.hextra-max-navbar-width { + max-width: 96rem; +} +#content.hextra-max-content-width, +main.hextra-max-content-width { + max-width: 80rem; +} + +/* Code block font-size: override Hextra’s default (.9em). Target the same wrappers Hextra uses. */ +.highlight pre, +.hextra-code-block, +.hextra-code-block pre { + font-size: 0.875rem; /* 14px; change to e.g. 0.8125rem (13px) or 1rem (16px) as needed */ +} diff --git a/docs/content/_index.md b/docs/content/_index.md new file mode 100644 index 0000000..b941bed --- /dev/null +++ b/docs/content/_index.md @@ -0,0 +1,66 @@ +--- +title: "Introduction" +description: "jooby-mcp integrates the MCP Java SDK with Jooby. Expose tools, prompts, and resources via annotations with build-time discovery." +type: docs +weight: 1 +--- + +

+ Maven Central + MvnRepository + MCP + MvnRepository +

+ +## What is jooby-mcp? + +**jooby-mcp** integrates the [Model Context Protocol (MCP) Java SDK](https://github.com/modelcontextprotocol/java-sdk) +with the [Jooby](https://github.com/jooby-project/jooby) framework. It lets you expose **tools**, **prompts**, and * +*resources** to MCP clients using declarative, annotation-based APIs—with discovery at **build time** via an annotation +processor, so no runtime reflection is required. + +## Prerequisites + +- A [Jooby](https://jooby.io) application (3.x or 4.x). +- Basic familiarity with MCP + concepts ([tools](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/tools/), [prompts](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/prompts/), [resources](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/resources/)) + is helpful but not required. + +## Version compatibility + +| Jooby | jooby-mcp | +|-------|-----------| +| 3.x | 1.x | +| 4.x | 2.x | + +## Features + +**Transport** + +- SSE, Streamable HTTP, and Stateless Streamable HTTP +- Multiple MCP servers in one application + +**MCP capabilities** + +- Tools, Prompts, Resources +- Resource templates with URI patterns and completions +- Prompt completions and resource-template completions + +**Quality & tooling** + +- Required argument validation and build-time checks for method signatures and return types +- Elicitation, Sampling, and Progress via the exchange object +- Optional [MCP Inspector]({{< ref "mcp-inspector" >}}) module for local testing + +## In this documentation + +| Section | Description | +|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| +| [Quick Start]({{< ref "quick-start" >}}) | Add the dependency, configure the server, and run your first MCP endpoint. | +| [Tools]({{< ref "tools" >}}), [Prompts]({{< ref "prompts" >}}), [Resources]({{< ref "resources" >}}) | Core MCP capabilities and annotations. | +| [Exchange object]({{< ref "exchange-object" >}}) | Elicitation, Sampling, and Progress. | +| [Multiple servers]({{< ref "multiple-servers" >}}), [Customizing server]({{< ref "customizing-server" >}}) | Run several servers and change the default key or package. | +| [MCP Inspector]({{< ref "mcp-inspector" >}}) | Local testing in the browser. | +| [Supported return types]({{< ref "appendix-return-types" >}}) | Reference for tools, prompts, and resources. | + +**Next:** [Quick Start]({{< ref "quick-start" >}}) — add the dependency and run your first MCP server. diff --git a/docs/content/appendix-return-types.md b/docs/content/appendix-return-types.md new file mode 100644 index 0000000..6770d28 --- /dev/null +++ b/docs/content/appendix-return-types.md @@ -0,0 +1,41 @@ +--- +title: "Supported Return Types" +description: "Return types supported for tools, prompts, and resources. String, McpSchema types, and POJOs." +type: docs +weight: 10 +--- + +This page lists the return types supported for each kind of handler. Use these so the annotation processor and runtime can serialize and expose results correctly. + +## Tools + +- `String` +- `McpSchema.CallToolResult` +- `McpSchema.Content` +- `McpSchema.TextContent` +- POJO (serialized to JSON) + +## Prompts + +- `McpSchema.GetPromptResult` +- `McpSchema.PromptMessage` +- `List` +- `McpSchema.Content` +- `String` +- POJO (string representation via `toString()`) + +## Resources and Resource Templates + +- `McpSchema.ReadResourceResult` +- `McpSchema.ResourceContents` +- `List` +- `McpSchema.TextResourceContents` +- `McpSchema.BlobResourceContents` +- POJO (serialized to JSON) + +## Completions + +- `McpSchema.CompleteResult` +- `McpSchema.CompleteResult.CompleteCompletion` +- `List` +- `String` \ No newline at end of file diff --git a/docs/content/customizing-server.md b/docs/content/customizing-server.md new file mode 100644 index 0000000..b9b4ef3 --- /dev/null +++ b/docs/content/customizing-server.md @@ -0,0 +1,47 @@ +--- +title: "Customizing Server Name and Package" +description: "Change the default MCP server key and generated class package via compiler arguments." +type: docs +weight: 8 +--- + +You can change the default MCP server key and the package of the generated server class via **compiler arguments**. That lets you align generated class names and config keys with your project (e.g. `CalculatorMcpServer` in `com.acme.corp.mcp`). + +## Maven configuration + +Add the arguments under the maven-compiler-plugin (next to the annotation processor path): + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.github.kliushnichenko + jooby-mcp-apt + ${version} + + + + -Amcp.default.server.key=calculator + -Amcp.target.package=com.acme.corp.mcp + + + +``` + +- **mcp.default.server.key** — Config key for the default server (e.g. `calculator` → `mcp.calculator` in config and a generated class name like `CalculatorMcpServer`). +- **mcp.target.package** — Package for generated server classes. + +## application.conf + +The server key in config must match the compiler argument: + +```hocon +mcp.calculator { + name: "calculator-mcp-server" + version: "0.1.0" + mcpEndpoint: "/mcp/calculator" +} +``` \ No newline at end of file diff --git a/docs/content/exchange-object.md b/docs/content/exchange-object.md new file mode 100644 index 0000000..3a19503 --- /dev/null +++ b/docs/content/exchange-object.md @@ -0,0 +1,26 @@ +--- +title: "Exchange Object" +description: "Use McpSyncServerExchange for Elicitation, Sampling, and Progress. Inject the exchange as a method argument." +type: docs +weight: 6 +--- + +The **McpSyncServerExchange** gives tool (and related) handlers access to the current request and to MCP features such as **Elicitation**, **Sampling**, and **Progress**. Add it as an argument to your method. The framework injects the current exchange. + +## Example + +```java +public class ElicitationExample { + + @Tool(name = "elicitation_example") + public String elicitationExample(McpSyncServerExchange exchange) { + // ... + exchange.createElicitation(request); + // ... + } +} +``` + +Use the same pattern for Sampling, Progress, and other exchange-based features. For a full example, see the [example project](https://github.com/kliushnichenko/jooby-mcp/blob/1.x/jooby-mcp-example/src/main/java/io/github/kliushnichenko/mcp/example/ElicitationExample.java). + +For details on the SDK APIs, see the [MCP Java SDK documentation](https://modelcontextprotocol.io/sdk/java/mcp-server#using-sampling-from-a-server). diff --git a/docs/content/mcp-inspector.md b/docs/content/mcp-inspector.md new file mode 100644 index 0000000..eb99831 --- /dev/null +++ b/docs/content/mcp-inspector.md @@ -0,0 +1,49 @@ +--- +title: "MCP Inspector" +description: "Embed the MCP Inspector UI in your Jooby app for local testing. Direct connection mode." +type: docs +weight: 9 +--- + +The **McpInspectorModule** embeds the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) UI in your Jooby app so you can test your MCP server in the browser. It uses **Direct** connection mode only and is intended for local development. **McpModule** must be installed first. + +## Setup + +Add the dependency: + +```xml + + io.github.kliushnichenko + jooby-mcp-inspector + ${jooby.mcp.version} + +``` + +Install the module next to **McpModule**: + +```java +{ + install(new McpInspectorModule()); +} +``` + +By default the inspector is served at **/mcp-inspector** and tries to auto-connect to the MCP server when the page loads. + +## Configuration + +You can change the path and disable auto-connect: + +```java +install(new McpInspectorModule() + .path("/custom-inspector-path") + .autoConnect(false)); +``` + +## Multiple servers + +When more than one MCP server is registered, choose which one the inspector connects to by default: + +```java +install(new McpInspectorModule() + .defaultMcpServer("weather-mcp-server")); // name from application.conf +``` diff --git a/docs/content/multiple-servers.md b/docs/content/multiple-servers.md new file mode 100644 index 0000000..bcafaee --- /dev/null +++ b/docs/content/multiple-servers.md @@ -0,0 +1,55 @@ +--- +title: "Multiple Servers" +description: "Run several MCP servers in one app with @McpServer. Assign tools and prompts to a server by key." +type: docs +weight: 7 +--- + +You can run several MCP servers in one application. Use **@McpServer** to assign tools or prompts to a specific server. The annotation processor generates a dedicated server class per key (e.g. `WeatherMcpServer` for `"weather"`). + +## 1. Annotate classes or methods + +Apply **@McpServer** at class or method level: + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.McpServer; +import io.github.kliushnichenko.jooby.mcp.annotation.Tool; + +@Singleton +@McpServer("weather") +public class WeatherService { + + public record Coordinates(double latitude, double longitude) {} + + @Tool(name = "get_weather") + public String getWeather(Coordinates coordinates) { + // ... + } +} +``` + +The processor generates **WeatherMcpServer** (and keeps **DefaultMcpServer** for the default key). + +## 2. Register all server instances + +Pass every generated server to **McpModule**: + +```java +{ + install(new McpModule(new DefaultMcpServer(), new WeatherMcpServer())); +} +``` + +## 3. Configure each server in application.conf + +Each server key has its own config block: + +```hocon +mcp.weather { + name: "weather-mcp-server" + version: "0.1.0" + mcpEndpoint: "/mcp/weather" +} +``` + +Use the same options as for the default server (transport, endpoints, etc.) as needed. diff --git a/docs/content/prompts.md b/docs/content/prompts.md new file mode 100644 index 0000000..1dc98c6 --- /dev/null +++ b/docs/content/prompts.md @@ -0,0 +1,72 @@ +--- +title: "Prompts" +description: "Expose prompt templates with @Prompt and @PromptArg. Optional prompt completions for suggested argument values." +type: docs +weight: 4 +--- + +Prompts expose template-style inputs to MCP clients (e.g. for LLM prompts). Annotate methods with **@Prompt**. You can optionally add **prompt completions** so clients get suggested values for arguments as the user types. + +## Defining a prompt + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.Prompt; +import io.github.kliushnichenko.jooby.mcp.annotation.PromptArg; + +@Singleton +public class PromptsExample { + + @Prompt(name = "summarizeText", description = "Summarizes the provided text into a specified number of sentences") + public String summarizeText( + @PromptArg(name = "text") String text, + String maxSentences + ) { + return String.format(""" + Please provide a clear and concise summary of the following text in no more than %s sentences: + %s + """, maxSentences, text); + } +} +``` + +- **@Prompt** — Registers the method as a prompt. Name and description can be inferred or set explicitly. +- **@PromptArg** — Describes parameters for the generated schema and client UX. + +## Prompt completions + +Prompt completions let clients get suggested values for a prompt’s arguments while the user is filling the form. Add a separate method annotated with **@CompletePrompt** that returns suggestions for a given argument and partial input. + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.CompleteArg; +import io.github.kliushnichenko.jooby.mcp.annotation.CompletePrompt; +import io.github.kliushnichenko.jooby.mcp.annotation.Prompt; + +@Singleton +public class PromptCompletionsExample { + + private static final List SUPPORTED_LANGUAGES = + List.of("Java", "Python", "JavaScript", "Go", "TypeScript"); + + @Prompt(name = "code_review", description = "Code Review Prompt") + public String codeReviewPrompt(String codeSnippet, String language) { + return """ + You are a senior software engineer tasked with reviewing the following %s code snippet: + %s + Please provide feedback on: + 1. Code readability and maintainability. + 2. Potential bugs or issues. + 3. Suggestions for improvement. + """.formatted(language, codeSnippet); + } + + @CompletePrompt("code_review") + public List completeCodeReviewLang(@CompleteArg(name = "language") String partialInput) { + return SUPPORTED_LANGUAGES.stream() + .filter(lang -> lang.toLowerCase().contains(partialInput.toLowerCase())) + .toList(); + } +} +``` + +- **@CompletePrompt("code_review")** — Ties this method to the prompt named `code_review`. +- **@CompleteArg(name = "language")** — This method completes the `language` argument. It receives the current partial input and returns a list of suggestions. diff --git a/docs/content/quick-start.md b/docs/content/quick-start.md new file mode 100644 index 0000000..c44b37f --- /dev/null +++ b/docs/content/quick-start.md @@ -0,0 +1,104 @@ +--- +title: "Quick Start" +description: "Add the jooby-mcp dependency, configure the annotation processor, and run your first MCP server with Jooby." +type: docs +weight: 2 +--- + +This guide gets you from zero to a running MCP server with jooby-mcp in a few steps. + +## 1. Add the dependency + +In your `pom.xml`: + +```xml + + io.github.kliushnichenko + jooby-mcp + ${jooby.mcp.version} + +``` + +## 2. Add the annotation processor + +Configure the Maven Compiler Plugin so the APT processor generates the MCP server class: + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.github.kliushnichenko + jooby-mcp-apt + ${jooby.mcp.version} + + + + +``` + +## 3. Configure the MCP server + +In `application.conf`, define at least one MCP server. The key (e.g. `default`) must match what the annotation processor expects (see [Customizing default server]({{< ref "customizing-server" >}}) to change it). + +> **Note:** Since **1.4.0** the default transport is **Streamable HTTP**, not SSE. Set `transport: "sse"` explicitly if you need SSE. + +**SSE transport** + +```hocon +mcp.default { + name: "my-awesome-mcp-server" # Required + version: "0.1.0" # Required + transport: "sse" # Optional (default: streamable-http) + sseEndpoint: "/mcp/sse" # Optional (default: /mcp/sse) + messageEndpoint: "/mcp/message" # Optional (default: /mcp/message) +} +``` + +**Streamable HTTP transport** (default) + +```hocon +mcp.default { + name: "my-awesome-mcp-server" + version: "0.1.0" + transport: "streamable-http" + mcpEndpoint: "/mcp/streamable" # Optional (default: /mcp) + disallowDelete: true # Optional (default: false) + keepAliveInterval: 45 # Optional, in seconds + instructions: "..." # Optional: server instructions for clients +} +``` + +**Stateless Streamable HTTP** + +```hocon +mcp.default { + name: "my-awesome-mcp-server" + version: "0.1.0" + transport: "stateless-streamable-http" + mcpEndpoint: "/mcp/stateless-streamable" # Optional (default: /mcp) +} +``` + +- **keepAliveInterval** — Sends periodic keep-alive messages when set to a positive number (seconds). Off by default. +- **instructions** — Shown to clients during initialization. Use it to describe how to use the server. + +## 4. Implement tools, prompts, or resources + +Add one or more classes with `@Tool`, `@Prompt`, or `@Resource` (and related) annotations. See [Tools]({{< ref "tools" >}}), [Prompts]({{< ref "prompts" >}}), and [Resources]({{< ref "resources" >}}) for examples, or browse the [example project](https://github.com/kliushnichenko/jooby-mcp/tree/main/jooby-mcp-example/src/main/java/io/github/kliushnichenko/jooby/mcp/example). + +## 5. Register the MCP module + +After building, the processor generates a `DefaultMcpServer` class (or a custom name if configured). Install the module in your Jooby app: + +```java +{ + install(new JacksonModule()); // Required for JSON-RPC + install(AvajeInjectModule.of()); // Or your preferred DI module + install(new McpModule(new DefaultMcpServer())); +} +``` + +Your MCP server is now available at the configured endpoint. Next, define [Tools]({{< ref "tools" >}}), [Prompts]({{< ref "prompts" >}}), or [Resources]({{< ref "resources" >}}). diff --git a/docs/content/resources.md b/docs/content/resources.md new file mode 100644 index 0000000..1ae5242 --- /dev/null +++ b/docs/content/resources.md @@ -0,0 +1,78 @@ +--- +title: "Resources" +description: "Expose content by URI with @Resource. Static resources and resource templates with URI patterns and completions." +type: docs +weight: 5 +--- + +Resources expose content to MCP clients by URI. You can expose **static resources** (one URI per method) or **resource templates** (a URI pattern with variables and optional completions). + +## Static resources + +Each method annotated with **@Resource** is registered as a resource the client can read by that URI. + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.Resource; + +@Singleton +public class ResourceExamples { + + @Resource(uri = "file:///project/README.md", name = "README.md", title = "README", mimeType = "text/markdown") + public McpSchema.TextResourceContents textResource() { + String content = """ + # Project Title + + This is an example README file for the project. + + ## Features + + - Feature 1 + - Feature 2 + - Feature 3 + """; + return new McpSchema.TextResourceContents("file:///project/README.md", "text/markdown", content); + } +} +``` + +- **uri** — Identifier clients use to request this resource. +- **name** / **title** — Metadata for listing and display. +- **mimeType** — Content type of the returned body. + +## Resource templates + +Resource templates expose a family of resources under a URI pattern (e.g. `file:///project/{name}`). You implement a handler that receives the template variables and returns the content, and optionally a **completion** method so clients can discover or suggest valid values for a variable (e.g. project names). + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.ResourceTemplate; +import io.github.kliushnichenko.jooby.mcp.annotation.CompleteResourceTemplate; +import io.github.kliushnichenko.jooby.mcp.annotation.CompleteArg; + +@Singleton +public class ResourceTemplateExamples { + + private static final Map PROJECTS = Map.of( + "project-alpha", "This is Project Alpha.", + "project-beta", "This is Project Beta.", + "project-gamma", "This is Project Gamma." + ); + + @ResourceTemplate(name = "get_project", uriTemplate = "file:///project/{name}") + public McpSchema.TextResourceContents getProject(String name, ResourceUri resourceUri) { + String content = PROJECTS.getOrDefault(name, ""); + return new McpSchema.TextResourceContents(resourceUri.uri(), "text/markdown", content); + } + + @CompleteResourceTemplate("get_project") + public List projectNameCompletion(@CompleteArg(name = "name") String partialInput) { + return PROJECTS.keySet().stream() + .filter(n -> n.contains(partialInput)) + .toList(); + } +} +``` + +- **@ResourceTemplate** — Binds a handler to a URI template. Method parameters map to template variables (and `ResourceUri` gives the resolved URI). +- **@CompleteResourceTemplate** — Binds a completion handler to that template. **@CompleteArg** maps parameters to template variables so the client can get suggestions as the user types. + +Return types for resources and templates must be one of the [supported resource return types]({{< ref "appendix-return-types" >}}#resources-and-resource-templates). For more examples, see the [example project](https://github.com/kliushnichenko/jooby-mcp/blob/1.x/jooby-mcp-example/src/main/java/io/github/kliushnichenko/mcp/example/ResourceExamples.java). diff --git a/docs/content/tools.md b/docs/content/tools.md new file mode 100644 index 0000000..be8f8cf --- /dev/null +++ b/docs/content/tools.md @@ -0,0 +1,76 @@ +--- +title: "Tools" +description: "Expose callable operations to MCP clients with @Tool and @ToolArg. Build-time discovery, validation, and schema generation." +type: docs +weight: 3 +--- + +Tools expose callable operations to MCP clients. Annotate methods with **@Tool**. The annotation processor discovers them at build time and registers them with the generated MCP server. + +## Example + +```java +import io.github.kliushnichenko.jooby.mcp.annotation.Tool; +import io.github.kliushnichenko.jooby.mcp.annotation.ToolArg; + +@Singleton +public class ToolsExample { + + @Tool(name = "add", description = "Adds two numbers together") + public String add( + @ToolArg(name = "first", description = "First number to add") int a, + @ToolArg(name = "second", description = "Second number to add") int b + ) { + return String.valueOf(a + b); + } + + @Tool + public String subtract(int a, int b) { + return String.valueOf(a - b); + } +} +``` + +- **@Tool** — Exposes the method as a tool. Name and description can be inferred or set explicitly. +- **@ToolArg** — Describes parameters for the generated JSON schema and client UX. + +## Output schema + +The output schema is derived from the method’s return type. For example, a tool that returns a `Pet` produces a schema that matches that class. + +If the return type is a reserved type (`String`, `McpSchema.CallToolResult`, or `McpSchema.Content`), no schema is generated automatically. Use **@OutputSchema** in that case: + +- **@OutputSchema.From(MyClass.class)** — Use the schema for that class. +- **@OutputSchema.ArrayOf(MyClass.class)** — Array of that class. +- **@OutputSchema.MapOf(MyClass.class)** — Map with that class as value type. + +Example for a tool that returns `CallToolResult` but should advertise a `Pet` schema: + +```java +@Tool(name = "find_pet", description = "Finds a pet by its ID") +@OutputSchema.From(Pet.class) +public McpSchema.CallToolResult findPet(String petId) { + // ... + return new McpSchema.CallToolResult(pet, false); +} +``` + +Use **@OutputSchema.Suppressed** to skip output schema generation when you don’t want to expose a structured result. + +## Enriching the JSON schema + +You can refine the generated schema (for both arguments and return types) with OpenAPI annotations. The processor respects: + +- **@Schema** — `description`, `requiredMode` (and related). +- **@JsonProperty** — The annotation’s `value` is used as the property name in the schema. + +```java +import io.swagger.v3.oas.annotations.media.Schema; +import com.fasterxml.jackson.annotation.JsonProperty; + +class User { + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The user's middle name") + @JsonProperty("middle-name") + private String middleName; +} +``` diff --git a/docs/go.mod b/docs/go.mod new file mode 100644 index 0000000..3cc0fdb --- /dev/null +++ b/docs/go.mod @@ -0,0 +1,5 @@ +module github.com/kliushnichenko/jooby-mcp/docs + +go 1.21 + +require github.com/imfing/hextra v0.12.1 // indirect diff --git a/docs/go.sum b/docs/go.sum new file mode 100644 index 0000000..a889b91 --- /dev/null +++ b/docs/go.sum @@ -0,0 +1,2 @@ +github.com/imfing/hextra v0.12.1 h1:3t1n0bmJbDzSTVfht93UDcfF1BXMRjeFojA071ri2l8= +github.com/imfing/hextra v0.12.1/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo= diff --git a/docs/hugo.yaml b/docs/hugo.yaml new file mode 100644 index 0000000..71d6e5b --- /dev/null +++ b/docs/hugo.yaml @@ -0,0 +1,62 @@ +baseURL: "https://kliushnichenko.github.io/jooby-mcp" +languageCode: "en-us" +title: "∞ jooby-mcp" + +module: + hugoVersion: + min: "0.146.0" + imports: + - path: "github.com/imfing/hextra" + +markup: + highlight: + noClasses: false + goldmark: + renderer: + unsafe: true + +menu: + main: + - name: "Documentation" + pageRef: "/" + weight: 10 + - name: "GitHub" + url: "https://github.com/kliushnichenko/jooby-mcp" + weight: 15 + - name: "Search" + weight: 20 + params: + type: "search" + +# Analytics: ID is set via env HUGO_SERVICES_GOOGLEANALYTICS_ID in CI (use secret GA_MEASUREMENT_ID) +services: + googleAnalytics: + ID: "" + +params: + description: "Jooby MCP - Integrate Java MCP SDK with Jooby framework. Annotation-based MCP tools, prompts, and resources for Jooby apps." + # Default image for Open Graph / social sharing (optional: add a 1200×630 image for better previews) + images: + - "images/mcp_logo.svg" + defaultTheme: "auto" + navbar: + displayTitle: true + displayLogo: true + logo: + path: images/mcp_logo.svg + dark: images/dark_mcp_logo.svg + link: / + width: 40 + height: 20 + footer: + enable: false + search: + enable: true + editURL: + enable: true + base: "https://github.com/kliushnichenko/jooby-mcp/edit/main/docs" + # Code block copy button (Hextra) + highlight: + copy: + enable: true + display: "hover" # "hover" or "always" diff --git a/docs/layouts/index.html b/docs/layouts/index.html new file mode 100644 index 0000000..c921e6c --- /dev/null +++ b/docs/layouts/index.html @@ -0,0 +1,25 @@ +{{ define "main" }} +
+ {{ partial "sidebar.html" (dict "context" .) }} + {{ partial "toc.html" . }} +
+
+ {{ partial "breadcrumb.html" (dict "page" . "enable" true) }} +
+ {{ if .Title }} +
+

{{ .Title }}

+ {{ partial "components/page-context-menu" . }} +
+ {{ end }} + {{ .Content }} +
+ {{ partial "components/last-updated.html" . }} + {{- if (site.Params.page.displayPagination | default true) -}} + {{- partial "components/pager.html" . -}} + {{- end -}} + {{ partial "components/comments.html" . }} +
+
+
+{{ end }} diff --git a/docs/layouts/partials/custom/head-end.html b/docs/layouts/partials/custom/head-end.html new file mode 100644 index 0000000..a4bad7d --- /dev/null +++ b/docs/layouts/partials/custom/head-end.html @@ -0,0 +1,18 @@ +{{- /* JSON-LD SoftwareApplication for search engines (home page only) */ -}} +{{- if .IsHome }} + +{{- end }} diff --git a/docs/layouts/robots.txt b/docs/layouts/robots.txt new file mode 100644 index 0000000..6b4c001 --- /dev/null +++ b/docs/layouts/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: {{ "sitemap.xml" | absURL }} diff --git a/docs/static/css/chroma-github.css b/docs/static/css/chroma-github.css new file mode 100644 index 0000000..4e8e43c --- /dev/null +++ b/docs/static/css/chroma-github.css @@ -0,0 +1,74 @@ +/* Generated using: hugo gen chromastyles --style=github */ + +/* Background */ .highlight .bg { background-color:#f7f7f7; } +/* PreWrapper */ .highlight .chroma { background-color:#f7f7f7;-webkit-text-size-adjust:none; } +/* Error */ .highlight .chroma .err { color:#f6f8fa;background-color:#82071e } +/* LineLink */ .highlight .chroma .lnlinks { outline:none;text-decoration:none;color:inherit } +/* LineTableTD */ .highlight .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; } +/* LineTable */ .highlight .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; } +/* LineHighlight */ .highlight .chroma .hl { background-color:#dedede } +/* LineNumbersTable */ .highlight .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } +/* LineNumbers */ .highlight .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f } +/* Line */ .highlight .chroma .line { display:flex; } +/* Keyword */ .highlight .chroma .k { color:#cf222e } +/* KeywordConstant */ .highlight .chroma .kc { color:#cf222e } +/* KeywordDeclaration */ .highlight .chroma .kd { color:#cf222e } +/* KeywordNamespace */ .highlight .chroma .kn { color:#cf222e } +/* KeywordPseudo */ .highlight .chroma .kp { color:#cf222e } +/* KeywordReserved */ .highlight .chroma .kr { color:#cf222e } +/* KeywordType */ .highlight .chroma .kt { color:#cf222e } +/* NameAttribute */ .highlight .chroma .na { color:#1f2328 } +/* NameClass */ .highlight .chroma .nc { color:#1f2328 } +/* NameConstant */ .highlight .chroma .no { color:#0550ae } +/* NameDecorator */ .highlight .chroma .nd { color:#0550ae } +/* NameEntity */ .highlight .chroma .ni { color:#6639ba } +/* NameLabel */ .highlight .chroma .nl { color:#900;font-weight:bold } +/* NameNamespace */ .highlight .chroma .nn { color:#24292e } +/* NameOther */ .highlight .chroma .nx { color:#1f2328 } +/* NameTag */ .highlight .chroma .nt { color:#0550ae } +/* NameBuiltin */ .highlight .chroma .nb { color:#6639ba } +/* NameBuiltinPseudo */ .highlight .chroma .bp { color:#6a737d } +/* NameVariable */ .highlight .chroma .nv { color:#953800 } +/* NameVariableClass */ .highlight .chroma .vc { color:#953800 } +/* NameVariableGlobal */ .highlight .chroma .vg { color:#953800 } +/* NameVariableInstance */ .highlight .chroma .vi { color:#953800 } +/* NameVariableMagic */ .highlight .chroma .vm { color:#953800 } +/* NameFunction */ .highlight .chroma .nf { color:#6639ba } +/* NameFunctionMagic */ .highlight .chroma .fm { color:#6639ba } +/* LiteralString */ .highlight .chroma .s { color:#0a3069 } +/* LiteralStringAffix */ .highlight .chroma .sa { color:#0a3069 } +/* LiteralStringBacktick */ .highlight .chroma .sb { color:#0a3069 } +/* LiteralStringChar */ .highlight .chroma .sc { color:#0a3069 } +/* LiteralStringDelimiter */ .highlight .chroma .dl { color:#0a3069 } +/* LiteralStringDoc */ .highlight .chroma .sd { color:#0a3069 } +/* LiteralStringDouble */ .highlight .chroma .s2 { color:#0a3069 } +/* LiteralStringEscape */ .highlight .chroma .se { color:#0a3069 } +/* LiteralStringHeredoc */ .highlight .chroma .sh { color:#0a3069 } +/* LiteralStringInterpol */ .highlight .chroma .si { color:#0a3069 } +/* LiteralStringOther */ .highlight .chroma .sx { color:#0a3069 } +/* LiteralStringRegex */ .highlight .chroma .sr { color:#0a3069 } +/* LiteralStringSingle */ .highlight .chroma .s1 { color:#0a3069 } +/* LiteralStringSymbol */ .highlight .chroma .ss { color:#032f62 } +/* LiteralNumber */ .highlight .chroma .m { color:#0550ae } +/* LiteralNumberBin */ .highlight .chroma .mb { color:#0550ae } +/* LiteralNumberFloat */ .highlight .chroma .mf { color:#0550ae } +/* LiteralNumberHex */ .highlight .chroma .mh { color:#0550ae } +/* LiteralNumberInteger */ .highlight .chroma .mi { color:#0550ae } +/* LiteralNumberIntegerLong */ .highlight .chroma .il { color:#0550ae } +/* LiteralNumberOct */ .highlight .chroma .mo { color:#0550ae } +/* Operator */ .highlight .chroma .o { color:#0550ae } +/* OperatorWord */ .highlight .chroma .ow { color:#0550ae } +/* Punctuation */ .highlight .chroma .p { color:#1f2328 } +/* Comment */ .highlight .chroma .c { color:#57606a } +/* CommentHashbang */ .highlight .chroma .ch { color:#57606a } +/* CommentMultiline */ .highlight .chroma .cm { color:#57606a } +/* CommentSingle */ .highlight .chroma .c1 { color:#57606a } +/* CommentSpecial */ .highlight .chroma .cs { color:#57606a } +/* CommentPreproc */ .highlight .chroma .cp { color:#57606a } +/* CommentPreprocFile */ .highlight .chroma .cpf { color:#57606a } +/* GenericDeleted */ .highlight .chroma .gd { color:#82071e;background-color:#ffebe9 } +/* GenericEmph */ .highlight .chroma .ge { color:#1f2328 } +/* GenericInserted */ .highlight .chroma .gi { color:#116329;background-color:#dafbe1 } +/* GenericOutput */ .highlight .chroma .go { color:#1f2328 } +/* GenericUnderline */ .highlight .chroma .gl { text-decoration:underline } +/* TextWhitespace */ .highlight .chroma .w { color:#fff } diff --git a/docs/static/css/chroma-tokyonight-moon.css b/docs/static/css/chroma-tokyonight-moon.css new file mode 100644 index 0000000..f70739c --- /dev/null +++ b/docs/static/css/chroma-tokyonight-moon.css @@ -0,0 +1,73 @@ +/* Tokyo Night Moon – dark mode only (scoped under .dark). Use .highlight for specificity. */ + +.dark .highlight .bg { color:#c8d3f5; background-color:#222436; } +.dark .highlight .chroma { color:#c8d3f5; background-color:#222436; -webkit-text-size-adjust:none; } +.dark .highlight .chroma .err { color:#c53b53 } +.dark .highlight .chroma .lnlinks { outline:none; text-decoration:none; color:inherit } +.dark .highlight .chroma .lntd { vertical-align:top; padding:0; margin:0; border:0; } +.dark .highlight .chroma .lntable { border-spacing:0; padding:0; margin:0; border:0; } +.dark .highlight .chroma .hl { background-color:#444a73 } +.dark .highlight .chroma .lnt { white-space:pre; -webkit-user-select:none; user-select:none; margin-right:0.4em; padding:0 0.4em 0 0.4em; color:#828bb8 } +.dark .highlight .chroma .ln { white-space:pre; -webkit-user-select:none; user-select:none; margin-right:0.4em; padding:0 0.4em 0 0.4em; color:#828bb8 } +.dark .highlight .chroma .line { display:flex; } +.dark .highlight .chroma .k { color:#c099ff } +.dark .highlight .chroma .kc { color:#ffc777 } +.dark .highlight .chroma .kd { color:#c099ff } +.dark .highlight .chroma .kn { color:#86e1fc } +.dark .highlight .chroma .kp { color:#c099ff } +.dark .highlight .chroma .kr { color:#c099ff } +.dark .highlight .chroma .kt { color:#4fd6be } +.dark .highlight .chroma .na { color:#82aaff } +.dark .highlight .chroma .nc { color:#ff966c } +.dark .highlight .chroma .no { color:#ff966c } +.dark .highlight .chroma .nd { color:#82aaff; font-weight:bold } +.dark .highlight .chroma .ni { color:#86e1fc } +.dark .highlight .chroma .ne { color:#ffc777 } +.dark .highlight .chroma .nl { color:#c3e88d } +.dark .highlight .chroma .nn { color:#ffc777 } +.dark .highlight .chroma .py { color:#ffc777 } +.dark .highlight .chroma .nt { color:#c099ff } +.dark .highlight .chroma .nb { color:#c3e88d } +.dark .highlight .chroma .bp { color:#c3e88d } +.dark .highlight .chroma .nf { color:#82aaff } +.dark .highlight .chroma .fm { color:#82aaff } +.dark .highlight .chroma .s { color:#c3e88d } +.dark .highlight .chroma .sa { color:#c099ff } +.dark .highlight .chroma .sb { color:#c3e88d } +.dark .highlight .chroma .sc { color:#c3e88d } +.dark .highlight .chroma .dl { color:#82aaff } +.dark .highlight .chroma .sd { color:#444a73 } +.dark .highlight .chroma .s2 { color:#c3e88d } +.dark .highlight .chroma .se { color:#82aaff } +.dark .highlight .chroma .sh { color:#444a73 } +.dark .highlight .chroma .si { color:#c3e88d } +.dark .highlight .chroma .sx { color:#c3e88d } +.dark .highlight .chroma .sr { color:#86e1fc } +.dark .highlight .chroma .s1 { color:#c3e88d } +.dark .highlight .chroma .ss { color:#c3e88d } +.dark .highlight .chroma .m { color:#ffc777 } +.dark .highlight .chroma .mb { color:#ffc777 } +.dark .highlight .chroma .mf { color:#ffc777 } +.dark .highlight .chroma .mh { color:#ffc777 } +.dark .highlight .chroma .mi { color:#ffc777 } +.dark .highlight .chroma .il { color:#ffc777 } +.dark .highlight .chroma .mo { color:#ffc777 } +.dark .highlight .chroma .o { color:#c3e88d; font-weight:bold } +.dark .highlight .chroma .ow { color:#c3e88d; font-weight:bold } +.dark .highlight .chroma .p { color:#c8d3f5; } +.dark .highlight .chroma .c { color:#444a73; font-style:italic } +.dark .highlight .chroma .ch { color:#444a73; font-style:italic } +.dark .highlight .chroma .cm { color:#444a73; font-style:italic } +.dark .highlight .chroma .c1 { color:#444a73; font-style:italic } +.dark .highlight .chroma .cs { color:#444a73; font-style:italic } +.dark .highlight .chroma .cp { color:#444a73; font-style:italic } +.dark .highlight .chroma .cpf { color:#444a73; font-weight:bold; font-style:italic } +.dark .highlight .chroma .gd { color:#c53b53; background-color:#1b1d2b } +.dark .highlight .chroma .ge { font-style:italic } +.dark .highlight .chroma .gr { color:#c53b53 } +.dark .highlight .chroma .gh { color:#ffc777; font-weight:bold } +.dark .highlight .chroma .gi { color:#c3e88d; background-color:#1b1d2b } +.dark .highlight .chroma .gs { font-weight:bold } +.dark .highlight .chroma .gu { color:#ffc777; font-weight:bold } +.dark .highlight .chroma .gt { color:#c53b53 } +.dark .highlight .chroma .gl { text-decoration:underline } diff --git a/docs/static/favicon.svg b/docs/static/favicon.svg new file mode 100644 index 0000000..3cde688 --- /dev/null +++ b/docs/static/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/static/images/dark_mcp_logo.svg b/docs/static/images/dark_mcp_logo.svg new file mode 100644 index 0000000..952964c --- /dev/null +++ b/docs/static/images/dark_mcp_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/static/images/mcp_logo.svg b/docs/static/images/mcp_logo.svg new file mode 100644 index 0000000..03d9f85 --- /dev/null +++ b/docs/static/images/mcp_logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + From e601386252c03454527568c735456d4294b83c42 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 16 Mar 2026 21:31:06 +0200 Subject: [PATCH 2/4] fixes --- README.md | 482 ++--------------------------------------- docs/content/_index.md | 11 +- docs/hugo.yaml | 2 +- 3 files changed, 25 insertions(+), 470 deletions(-) diff --git a/README.md b/README.md index fce6ce3..dc8d7e9 100644 --- a/README.md +++ b/README.md @@ -1,477 +1,33 @@ [![Maven Central](https://img.shields.io/maven-central/v/io.github.kliushnichenko/jooby-mcp.svg)](https://central.sonatype.com/search?q=jooby-mcp) -[![Javadoc](https://javadoc.io/badge/io.github.kliushnichenko/jooby-mcp/jooby-mcp.svg)](https://javadoc.io/doc/io.github.kliushnichenko/jooby-mcp/latest) [![Github](https://github.com/kliushnichenko/jooby-mcp/actions/workflows/maven.yml/badge.svg)](https://github.com/kliushnichenko/jooby-mcp/actions) +[![Documentation](https://img.shields.io/badge/documentation-site-green)](https://kliushnichenko.github.io/jooby-mcp/) +[![Javadoc](https://javadoc.io/badge/io.github.kliushnichenko/jooby-mcp/jooby-mcp.svg)](https://javadoc.io/doc/io.github.kliushnichenko/jooby-mcp/latest) ## jooby-mcp -This module integrates the official [Java MCP SDK](https://github.com/modelcontextprotocol/java-sdk) with [Jooby](https://github.com/jooby-project/jooby)’s routing, server features, and dependency injection. - -This module enables declarative (annotation-based) registration of tools, prompts, and resources. -Annotations are discovered at build time using APT, eliminating the need for runtime reflection. -To use it, add the annotation processor alongside the module dependency. - -Compatibility: - -| Jooby Version | Jooby MCP Version | -|---------------|-------------------| -| 3.x | 1.x | -| 4.x | 2.x | - -Features: - -- [X] SSE, Streamable-HTTP and Stateless Streamable-HTTP transport -- [X] Multiple servers support -- [X] Tools -- [X] Prompts -- [X] Resources -- [X] Resource Templates -- [X] Prompt Completions -- [X] Resource Template Completions -- [X] Required input arguments validation in tools -- [X] Build time method signature and return type validation -- [X] Elicitation, Sampling, Progress support via exchange object -- [X] MCP Inspector integration module - -Table of Contents: - -- [Quick Start](#quick-start) -- [Tools & Prompts Example](#tools--prompts-example) -- [Resource Example](#resource-example) -- [Resource Template Example](#resource-template-example) -- [Prompt Completion Example](#prompt-completion-example) -- [Exchange Object](#exchange-object) -- [Multiple Servers Support](#multiple-servers-support) -- [Customizing Default Server Name and Package](#customizing-default-server-name-and-package) -- [MCP Inspector Module](#mcp-inspector-module) -- [Appendix: Supported Return Types](#appendix-supported-return-types) - -## Quick Start - -1. Add the dependency to your `pom.xml`: - ```xml - - io.github.kliushnichenko - jooby-mcp - ${jooby.mcp.version} - - ``` -2. Add annotation processor - ```xml - - org.apache.maven.plugins - maven-compiler-plugin - - - - io.github.kliushnichenko - jooby-mcp-apt - ${jooby.mcp.version} - - - - - ``` - -3. Add configuration to `application.conf` - ⚠️ Since version `1.4.0` default transport was changed from `SSE` to `Streamable HTTP` - ``` - mcp.default { # `default` is the server key, can be customized over compiler arguments - name: "my-awesome-mcp-server" # Required - version: "0.1.0" # Required - transport: "sse" # Optional (default: streamable-http) - sseEndpoint: "/mcp/sse" # Optional (default: /mcp/sse), applicable only to SSE transport - messageEndpoint: "/mcp/message" # Optional (default: /mcp/message), applicable only to SSE transport - } - ``` - - Full config for `Streamable HTTP` transport: - ``` - mcp.default { - name: "my-awesome-mcp-server" # Required - version: "0.1.0" # Required - transport: "streamable-http" # Optional (default: streamable-http) - mcpEndpoint: "/mcp/streamable" # Optional (default: /mcp), applicable only to Streamable HTTP transport - disallowDelete: true # Optional (default: false) - keepAliveInterval: 45 # Optional (default: N/A), in seconds - instructions: "..." # Optional - } - ``` - - Full config for `Stateless Streamable HTTP` transport: - ``` - mcp.default { - name: "my-awesome-mcp-server" - version: "0.1.0" - transport: "stateless-streamable-http" - mcpEndpoint: "/mcp/stateless-streamable" # Optional (default: /mcp) - } - ``` - - *`keepAliveInterval` - enables sending periodic keep-alive messages to the client. - Disabled by default to avoid excessive network overhead. Set to a positive integer value (in seconds) to enable. - - *`instructions` - Sets the server instructions that will be shared with clients during connection initialization. These instructions provide guidance to the client on how to interact with this server. - -4. Implement your features (tools, prompts, resources, etc.), see examples below or in - the [example-project](https://github.com/kliushnichenko/jooby-mcp/blob/1.x/jooby-mcp-example/src/main/java/io/github/kliushnichenko/mcp/example) - -5. Install the module. After compilation, you can observe generated `DefaultMcpServer` class. Now register its instance in the module: - ```java - { - install(new JacksonModule()); // a JSON encode/decoder is required for JSONRPC - install(AvajeInjectModule.of()); // or other DI module of your choice - install(new McpModule(new DefaultMcpServer()); // register MCP server - } - ``` - -### Tools & Prompts Example - -```java -import io.github.kliushnichenko.jooby.mcp.annotation.Tool; -import io.github.kliushnichenko.jooby.mcp.annotation.ToolArg; -import io.github.kliushnichenko.jooby.mcp.annotation.Prompt; - -@Singleton -public class ToolsAndPromptsExample { - - @Tool(name = "add", description = "Adds two numbers together") - public String add( - @ToolArg(name = "first", description = "First number to add") int a, - @ToolArg(name = "second", description = "Second number to add") int b - ) { - int result = a + b; - return String.valueOf(result); - } - - @Tool - public String subtract(int a, int b) { - int result = a - b; - return String.valueOf(result); - } - - @Prompt(name = "summarizeText", description = "Summarizes the provided text into a specified number of sentences") - public String summarizeText(@PromptArg(name = "text") String text, String maxSentences) { - return String.format(""" - Please provide a clear and concise summary of the following text in no more than %s sentences: - %s - """, maxSentences, text); - } -} -``` - -#### Tools Output Schema - -The output schema is automatically generated based on the return type of the method. -For example, for the `find_pet` tool below - -```java -@Tool(name = "find_pet", description = "Finds a pet by its ID") -public Pet findPet(String petId) { - ... -} -``` -the generated output schema will reflect the `Pet` class structure. - -If the return type is one of the reserved types (`String`, `McpSchema.CallToolResult`, `McpSchema.Content`) -auto-generation step is omitted. -Use `@OutputSchema` annotation to explicitly define the output schema in such cases: -- `@OutputSchema.From(Pet.class)` - to use schema generated from a specific class -- `@OutputSchema.ArrayOf(Pet.class)` - to use array of specific class as output schema -- `@OutputSchema.MapOf(Pet.class)` - to use map of specific class as output schema - -Example: -```java -@Tool(name = "find_pet", description = "Finds a pet by its ID") -@OutputSchema.From(Pet.class) -public McpSchema.CallToolResult findPet(String petId) { - ... - - return new McpSchema.CallToolResult(pet, false); -} -``` - -**Tip**: You can use `@OutputSchema.Suppressed` to skip output schema generation from return type. - -### How to enrich json schema? - -Whether the schema is generated from method arguments or return type, you can enrich it using OpenAPI annotations. At the moment, generator will respect `description` and `required` attributes from `@Schema` annotation. Also, if `@JsonProperty` is present on a field, its `value` will be used as the property name in the generated schema. - -```java -import io.swagger.v3.oas.annotations.media.Schema; -class User { - @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The user's middle name") - @JsonProperty("middle-name") - private String middleName; -} -``` - -### Resource Example - -```java -import io.github.kliushnichenko.jooby.mcp.annotation.Resource; - -@Singleton -public class ResourceExamples { - - @Resource(uri = "file:///project/README.md", name = "README.md", title = "README", mimeType = "text/markdown") - public McpSchema.TextResourceContents textResource() { - String content = """ - # Project Title - - This is an example README file for the project. - - ## Features - - - Feature 1 - - Feature 2 - - Feature 3 - - """; - return new McpSchema.TextResourceContents("file:///project/README.md", "text/markdown", content); - } -} -``` - -Find more examples in the [example-project](https://github.com/kliushnichenko/jooby-mcp/blob/1.x/jooby-mcp-example/src/main/java/io/github/kliushnichenko/mcp/example/ResourceExamples.java) - -### Resource Template Example - -```java -import io.github.kliushnichenko.jooby.mcp.annotation.ResourceTemplate; -import io.github.kliushnichenko.jooby.mcp.annotation.CompleteResourceTemplate; - -@Singleton -public class ResourceTemplateExamples { - - private static final Map PROJECTS = Map.of( - "project-alpha", "This is Project Alpha.", - "project-beta", "This is Project Beta.", - "project-gamma", "This is Project Gamma." - ); - - @ResourceTemplate(name = "get_project", uriTemplate = "file:///project/{name}") - public McpSchema.TextResourceContents getProject(String name, ResourceUri resourceUri) { - String content = PROJECTS.getOrDefault(name, ""); - return new McpSchema.TextResourceContents(resourceUri.uri(), "text/markdown", content); - } - - @CompleteResourceTemplate("get_project") - public List projectNameCompletion(@CompleteArg(name = "name") String partialInput) { - return PROJECTS.keySet() - .stream() - .filter(name -> name.contains(partialInput)) - .toList(); - } -} -``` - -### Prompt Completion Example - -```java -import io.github.kliushnichenko.jooby.mcp.annotation.CompleteArg; -import io.github.kliushnichenko.jooby.mcp.annotation.CompletePrompt; -import io.github.kliushnichenko.jooby.mcp.annotation.Prompt; - -@Singleton -public class PromptCompletionsExample { - - private static final List SUPPORTED_LANGUAGES = List.of("Java", "Python", "JavaScript", "Go", "TypeScript"); - - @Prompt(name = "code_review", description = "Code Review Prompt") - public String codeReviewPrompt(String codeSnippet, String language) { - return """ - You are a senior software engineer tasked with reviewing the following %s code snippet: - - %s - - Please provide feedback on: - 1. Code readability and maintainability. - 2. Potential bugs or issues. - 3. Suggestions for improvement. - """.formatted(language, codeSnippet); - } - - @CompletePrompt("code_review") - public List completeCodeReviewLang(@CompleteArg(name = "language") String partialInput) { - return SUPPORTED_LANGUAGES.stream() - .filter(lang -> lang.toLowerCase().contains(partialInput.toLowerCase())) - .toList(); - } -} -``` -### Exchange Object - -An exchange object can be used to access request metadata, and support `Elicitation`, `Sampling` or `Progress` features. -Add `McpSyncServerExchange` argument to your tool method: - -```java -public class ElicitationExample { - - @Tool(name = "elicitation_example") - public String elicitationExample(McpSyncServerExchange exchange) { - ... - exchange.createElicitation(request); - ... - } -} -``` -See full example at [example-project](https://github.com/kliushnichenko/jooby-mcp/blob/1.x/jooby-mcp-example/src/main/java/io/github/kliushnichenko/mcp/example/ElicitationExample.java) - -In the similar manner you can support Sampling, Progress and other features by using `McpSyncServerExchange` object. -Explore SDK documentation for more details: https://modelcontextprotocol.io/sdk/java/mcp-server#using-sampling-from-a-server - -### Multiple Servers Support - -Use `@McpServer` annotation to assign a tool or prompt to a specific server. Annotation can be applied at the class or -method level. - - ```java - import io.github.kliushnichenko.jooby.mcp.annotation.McpServer; - -@Singleton -@McpServer("weather") -public class WeatherService { - - public record Coordinates(double latitude, double longitude) { - } - - @Tool(name = "get_weather") - public String getWeather(Coordinates coordinates) { - ... - } -} - ``` - -As a result, additional `WeatherMcpServer` class will be generated. Register it in the module: - - ```java - { - install(new McpModule(new DefaultMcpServer(),new WeatherMcpServer())); - } - ``` - -The weather MCP server should have its own configuration section in `application.conf`: - - ``` - mcp.weather { - name: "weather-mcp-server" - version: "0.1.0" - mcpEndpoint: "/mcp/weather" - } - ``` - -### Customizing Default Server Name and Package - -You can customize the default server name and the package where the generated server classes will be placed by providing -compiler arguments. For example, to set the default server name to `CalculatorMcpServer` and the package -to `com.acme.corp.mcp`, you can add the following configuration to your `pom.xml`: - - ```xml - - - org.apache.maven.plugins - maven-compiler-plugin - - - - io.github.kliushnichenko - jooby-mcp-apt - ${version} - - - - -Amcp.default.server.key=calculator - -Amcp.target.package=com.acme.corp.mcp - - - - ``` - -Mind, that `mcp.default.server.key` should match the configuration section in `application.conf`: - - ``` - mcp.calculator { - name: "calculator-mcp-server" - version: "0.1.0" - mcpEndpoint: "/mcp/calculator" - } - ``` - -### MCP Inspector Module - -You can use the `McpInspectorModule` to spin up [mcp-inspector-ui](https://github.com/modelcontextprotocol/inspector) right on jooby server -and speed up your development process. Mind, that it works in `Direct` connection mode only, -aimed solely to test your local MCP server during development and requires `McpModule` to be installed. -To enable it, just install the module alongside `McpModule`: - -```java -{ - install(new McpInspectorModule()); -} -``` - -By default, inspector will be available at `/mcp-inspector` path. And it will try to auto-connect to MCP server upon loading. -You can customize both options over corresponding mutators: - -```java -{ - install(new McpInspectorModule() - .path("/custom-inspector-path") - .autoConnect(false); - ); -} -``` - -If you have more than one MCP server registered, you can specify which one to connect to by default: - -```java -{ - install(new McpInspectorModule() - .defaultMcpServer("weather-mcp-server") // server name from application.conf settings - ); -} -``` - -Dependency: -```xml - - io.github.kliushnichenko - jooby-mcp-inspector - ${jooby.mcp.version} - -``` +**jooby-mcp** integrates the [Model Context Protocol (MCP) Java SDK](https://github.com/modelcontextprotocol/java-sdk) +with the [Jooby](https://github.com/jooby-project/jooby) framework. It lets you expose **tools**, **prompts**, and **resources** to MCP clients using declarative, annotation-based APIs with discovery at **build time** via an annotation +processor, so no runtime reflection is required. -### Appendix: Supported Return Types +⭐ **If you find this project useful, consider giving it a star. It helps others discover it** -##### Supported return types in Tools +🚀 Quickstart, guides, and API reference available in the [docs](https://kliushnichenko.github.io/jooby-mcp/) -- `String` -- `McpSchema.CallToolResult` -- `McpSchema.Content` -- `McpSchema.TextContent` -- POJO (will be serialized to JSON) +### Features -##### Supported return types in Prompts +**Transport** -- `McpSchema.GetPromptResult` -- `McpSchema.PromptMessage` -- `List` -- `McpSchema.Content` -- `String` -- POJO (`toString()` method will be invoked to get the string representation) +- SSE, Streamable HTTP, and Stateless Streamable HTTP +- Multiple MCP servers in one application -##### Supported return types in Resources and Resource Templates +**MCP capabilities** -- `McpSchema.ReadResourceResult` -- `McpSchema.ResourceContents` -- `List` -- `McpSchema.TextResourceContents` -- `McpSchema.BlobResourceContents` -- POJO (will be serialized to JSON) +- Tools, Prompts, Resources +- Resource templates with URI patterns and completions +- Prompt completions and resource-template completions -##### Supported return types in Completions +**Quality & tooling** -- `McpSchema.CompleteResult` -- `McpSchema.CompleteResult.CompleteCompletion` -- `List` -- `String` +- Required argument validation and build-time checks for method signatures and return types +- Elicitation, Sampling, and Progress via the exchange object +- Optional `MCP Inspector` module for local testing diff --git a/docs/content/_index.md b/docs/content/_index.md index b941bed..6e9e175 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -6,24 +6,23 @@ weight: 1 ---

- Maven Central - MvnRepository + Maven Central + MvnRepository MCP - MvnRepository + Javadoc

## What is jooby-mcp? **jooby-mcp** integrates the [Model Context Protocol (MCP) Java SDK](https://github.com/modelcontextprotocol/java-sdk) -with the [Jooby](https://github.com/jooby-project/jooby) framework. It lets you expose **tools**, **prompts**, and * -*resources** to MCP clients using declarative, annotation-based APIs—with discovery at **build time** via an annotation +with the [Jooby](https://github.com/jooby-project/jooby) framework. It lets you expose **tools**, **prompts**, and **resources** to MCP clients using declarative, annotation-based APIs with discovery at **build time** via an annotation processor, so no runtime reflection is required. ## Prerequisites - A [Jooby](https://jooby.io) application (3.x or 4.x). - Basic familiarity with MCP - concepts ([tools](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/tools/), [prompts](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/prompts/), [resources](https://spec.modelcontextprotocol.io/specification/2025-05-26/basic/resources/)) + concepts ([tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools), [prompts](https://modelcontextprotocol.io/specification/2025-11-25/server/prompts), [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources)) is helpful but not required. ## Version compatibility diff --git a/docs/hugo.yaml b/docs/hugo.yaml index 71d6e5b..3f8853c 100644 --- a/docs/hugo.yaml +++ b/docs/hugo.yaml @@ -45,7 +45,7 @@ params: logo: path: images/mcp_logo.svg dark: images/dark_mcp_logo.svg - link: / + link: /jooby-mcp/ width: 40 height: 20 footer: From d27c2399302fde1f8cd6afeeff79658f2e9c8734 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 16 Mar 2026 21:37:25 +0200 Subject: [PATCH 3/4] fixes --- README.md | 2 +- docs/content/_index.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dc8d7e9..ca48182 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ processor, so no runtime reflection is required. ⭐ **If you find this project useful, consider giving it a star. It helps others discover it** -🚀 Quickstart, guides, and API reference available in the [docs](https://kliushnichenko.github.io/jooby-mcp/) +🚀 Quickstart, guides, and API reference available in the docs ### Features diff --git a/docs/content/_index.md b/docs/content/_index.md index 6e9e175..65f840c 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -6,10 +6,10 @@ weight: 1 ---

- Maven Central - MvnRepository + Maven Central + MvnRepository MCP - Javadoc + Javadoc

## What is jooby-mcp? From f45ec45c72ce65786af47e177611e54ed88e5c65 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 16 Mar 2026 21:40:25 +0200 Subject: [PATCH 4/4] cleanup --- .github/workflows/hugo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index fd0683e..314f31f 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -2,7 +2,7 @@ name: Deploy documentation to GitHub Pages on: push: - branches: ["main", "feat/docs"] + branches: ["main"] paths: - "docs/**" - ".github/workflows/hugo.yml"