diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671b7a31e..d1845b125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,11 @@ jobs: with: node-version: "20" + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - run: npm install - run: npm run build @@ -109,12 +114,12 @@ jobs: with: distribution: Ubuntu-24.04 - - name: Install Node.js in WSL + - name: Install Node.js and Java in WSL shell: wsl-bash {0} run: | sudo apt-get update curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - - sudo apt-get install -y nodejs + sudo apt-get install -y nodejs openjdk-17-jdk-headless maven - name: Build and test in WSL shell: wsl-bash {0} diff --git a/README.md b/README.md index e1429330c..5a3f06b78 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,18 @@ The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/exa | | | |:---:|:---| | [![Basic](examples/basic-server-react/grid-cell.png "Starter template")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) | The same app built with different frameworks — pick your favorite!

[React](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) · [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) · [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) · [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) · [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) · [Vanilla JS](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) | +| [![Basic](examples/basic-server-react/grid-cell.png "Inlined Java")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/inlined-server-java) | [**Inlined Java**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/inlined-server-java) — MCP App server in Java with the UI inlined as an HTML string (no frontend build step). Loads the SDK from CDN. | ### Running the Examples +#### Prerequisites + +Most examples require only Node.js 18+. A few have additional requirements: + +- **Python examples** (`qr-server`, `say-server`): [uv](https://docs.astral.sh/uv/getting-started/installation/) +- **Java example** (`inlined-server-java`): Java 17+ and Maven 3.6+ + #### With basic-host To run all examples locally using [basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) (the reference host implementation included in this repo): @@ -306,6 +314,13 @@ To use these examples with MCP clients that support the stdio transport (such as "--stdio" ] }, + "basic-java": { + "command": "bash", + "args": [ + "-c", + "cd /path/to/ext-apps/examples/inlined-server-java && mvn -B package -DskipTests -q >&2 && java -jar target/inlined-server-java-1.0.0.jar --stdio" + ] + }, "qr": { "command": "uv", "args": [ @@ -331,7 +346,7 @@ To use these examples with MCP clients that support the stdio transport (such as > [!NOTE] -> The `qr` server requires cloning the repository first. See [qr-server README](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) for details. +> The `qr` and `basic-java` servers require cloning the repository first. See their README files for details. #### Local Development @@ -484,6 +499,13 @@ Then configure your MCP client to build and run the local server. Replace `~/cod "cd ~/code/ext-apps/examples/wiki-explorer-server && npm run build >&2 && node dist/index.js --stdio" ] }, + "basic-java": { + "command": "bash", + "args": [ + "-c", + "cd ~/code/ext-apps/examples/inlined-server-java && mvn -B package -DskipTests -q >&2 && java -jar target/inlined-server-java-1.0.0.jar --stdio" + ] + }, "qr": { "command": "bash", "args": [ diff --git a/examples/inlined-server-java/package.json b/examples/inlined-server-java/package.json new file mode 100644 index 000000000..d571cba51 --- /dev/null +++ b/examples/inlined-server-java/package.json @@ -0,0 +1,11 @@ +{ + "name": "@modelcontextprotocol/server-inlined-java", + "version": "1.0.0", + "private": true, + "description": "MCP App server in Java with inlined HTML UI (no frontend build step)", + "scripts": { + "build": "mvn -B package -DskipTests -q", + "start": "mvn -B package -DskipTests -q && java -jar target/inlined-server-java-1.0.0.jar", + "dev": "mvn -B package -DskipTests -q && java -jar target/inlined-server-java-1.0.0.jar" + } +} diff --git a/examples/inlined-server-java/pom.xml b/examples/inlined-server-java/pom.xml new file mode 100644 index 000000000..994bf20c6 --- /dev/null +++ b/examples/inlined-server-java/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + io.modelcontextprotocol.examples + inlined-server-java + 1.0.0 + jar + + + 21 + ${java.version} + ${java.version} + UTF-8 + + + + + + io.modelcontextprotocol.sdk + mcp + 0.17.2 + + + + + org.eclipse.jetty + jetty-server + 12.0.16 + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + 12.0.16 + + + + + org.slf4j + slf4j-simple + 2.0.16 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + shade + + false + + + io.modelcontextprotocol.examples.Main + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Main.java b/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Main.java new file mode 100644 index 000000000..ca79d88d6 --- /dev/null +++ b/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Main.java @@ -0,0 +1,54 @@ +package io.modelcontextprotocol.examples; + +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapperSupplier; +import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; + +import java.util.EnumSet; +import java.util.List; + +/** + * Entry point for the inlined-server-java MCP App example. + * + * HTTP (default): java -jar inlined-server-java.jar + * Stdio: java -jar inlined-server-java.jar --stdio + */ +public class Main { + + public static void main(String[] args) throws Exception { + var json = new JacksonMcpJsonMapperSupplier().get(); + + if (List.of(args).contains("--stdio")) { + Server.create(new StdioServerTransportProvider(json)); + return; + } + + int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "3001")); + var transport = HttpServletStatelessServerTransport.builder().jsonMapper(json).build(); + Server.create(transport); + + var context = new ServletContextHandler(); + context.addFilter(new FilterHolder((Filter) (req, res, chain) -> { + ((HttpServletResponse) res).setHeader("Access-Control-Allow-Origin", "*"); + ((HttpServletResponse) res).setHeader("Access-Control-Allow-Headers", "*"); + if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) { + ((HttpServletResponse) res).setStatus(200); + return; + } + chain.doFilter(req, res); + }), "/*", EnumSet.of(DispatcherType.REQUEST)); + context.addServlet(new ServletHolder(transport), "/mcp"); + + var server = new org.eclipse.jetty.server.Server(port); + server.setHandler(context); + server.start(); + System.out.println("MCP server listening on http://localhost:" + port + "/mcp"); + server.join(); + } +} diff --git a/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Server.java b/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Server.java new file mode 100644 index 000000000..7adf267c3 --- /dev/null +++ b/examples/inlined-server-java/src/main/java/io/modelcontextprotocol/examples/Server.java @@ -0,0 +1,86 @@ +package io.modelcontextprotocol.examples; + +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpStatelessServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.spec.McpStatelessServerTransport; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * MCP server definition: registers a "get-time" tool with an inline HTML UI resource. + */ +public class Server { + + static final String RESOURCE_URI = "ui://get-time/index.html"; + static final String RESOURCE_MIME = "text/html;profile=mcp-app"; + + static final String UI_HTML = """ + + + + + + Get Time + + +

Server time:

+ + + + """; + + static final McpSchema.Tool TOOL = McpSchema.Tool.builder() + .name("get-time") + .description("Returns the current server time as an ISO 8601 string") + .inputSchema(new McpSchema.JsonSchema("object", null, null, null, null, null)) + .meta(Map.of("ui", Map.of("resourceUri", RESOURCE_URI))) + .build(); + + static final McpSchema.Resource RESOURCE = McpSchema.Resource.builder() + .uri(RESOURCE_URI).name("Get Time UI").mimeType(RESOURCE_MIME).build(); + + static McpSchema.CallToolResult getTime() { + var time = Instant.now().toString(); + return McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(time))) + .structuredContent(Map.of("time", time)) + .build(); + } + + static McpSchema.ReadResourceResult readResource() { + return new McpSchema.ReadResourceResult(List.of(new McpSchema.TextResourceContents( + RESOURCE_URI, RESOURCE_MIME, UI_HTML, + Map.of("ui", Map.of("csp", Map.of("resourceDomains", List.of("https://unpkg.com"))))))); + } + + /** Stateful server (stdio). */ + static void create(McpServerTransportProvider transport) { + McpServer.sync(transport) + .serverInfo("inlined-server-java", "1.0.0") + .tools(new McpServerFeatures.SyncToolSpecification(TOOL, (ex, a) -> getTime())) + .resources(new McpServerFeatures.SyncResourceSpecification(RESOURCE, (ex, r) -> readResource())) + .build(); + } + + /** Stateless server (HTTP, matches JS examples). */ + static void create(McpStatelessServerTransport transport) { + McpServer.sync(transport) + .serverInfo("inlined-server-java", "1.0.0") + .tools(new McpStatelessServerFeatures.SyncToolSpecification(TOOL, (ctx, r) -> getTime())) + .resources(new McpStatelessServerFeatures.SyncResourceSpecification(RESOURCE, (ctx, r) -> readResource())) + .build(); + } +} diff --git a/package-lock.json b/package-lock.json index 7aa9ef932..ffc2df8a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -557,6 +557,10 @@ "dev": true, "license": "MIT" }, + "examples/inlined-server-java": { + "name": "@modelcontextprotocol/server-inlined-java", + "version": "1.0.0" + }, "examples/integration-server": { "version": "1.0.0", "dependencies": { @@ -2657,6 +2661,10 @@ "resolved": "examples/debug-server", "link": true }, + "node_modules/@modelcontextprotocol/server-inlined-java": { + "resolved": "examples/inlined-server-java", + "link": true + }, "node_modules/@modelcontextprotocol/server-map": { "resolved": "examples/map-server", "link": true