001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.examples.mcp; 018 019import java.net.*; 020 021import org.apache.juneau.commons.inject.*; 022import org.apache.juneau.microservice.*; 023import org.apache.juneau.microservice.jetty.*; 024import org.eclipse.jetty.ee11.servlet.*; 025import org.eclipse.jetty.server.*; 026 027import jakarta.servlet.*; 028 029/** 030 * Boots {@link ExampleMcpServer} on an embedded Jetty server. 031 * 032 * <p> 033 * Run {@link #main(String[]) main} to start the server on a fixed port (default {@code 10000}, or pass a 034 * port as the first argument) and leave it running so you can point {@link ExampleClient} (or any MCP 035 * client) at it. The in-process end-to-end test uses {@link #start(int) start(0)} to boot on an 036 * OS-assigned ephemeral port instead. 037 * 038 * <p> 039 * The wiring mirrors Juneau's own test fixture: a Jetty {@link Server} plus the {@link ExampleMcpServer} 040 * servlet are placed in a {@link BasicBeanStore}, and {@link JettyConfiguration} (added via 041 * {@link org.apache.juneau.microservice.Microservice.Builder#configurations(Class...)}) auto-mounts the {@link org.apache.juneau.rest.server.Rest @Rest} 042 * servlet at {@code "/"} and drives the server lifecycle. Because the {@code Server} is supplied as a bean, 043 * we control the listen port directly — no {@code jetty.xml} required. 044 */ 045@SuppressWarnings({ 046 "java:S106", // Example walkthrough intentionally prints to stdout; console output is the demo's deliverable. 047 "resource" // start() returns the running ExampleServer to the caller, who closes it; main() intentionally runs until the process is killed 048}) 049public final class ExampleServer implements AutoCloseable { 050 051 /** Default listen port used by {@link #main(String[])} when none is supplied. */ 052 public static final int DEFAULT_PORT = 10000; 053 054 private final Microservice microservice; 055 private final URI rootUrl; 056 057 private ExampleServer(Microservice microservice, URI rootUrl) { 058 this.microservice = microservice; 059 this.rootUrl = rootUrl; 060 } 061 062 /** 063 * Starts the example MCP server. 064 * 065 * @param port The TCP port to listen on, or {@code 0} to let the OS assign an ephemeral port. 066 * @return A running server handle. Close it (or call {@link #close()}) to stop. 067 * @throws Exception If the server fails to start. 068 */ 069 public static ExampleServer start(int port) throws Exception { 070 var jetty = buildServer(port); 071 072 var beanStore = new BasicBeanStore(); 073 beanStore.addBean(Server.class, jetty); 074 beanStore.addBean(Servlet.class, new ExampleMcpServer()); 075 076 var microservice = Microservice.create() 077 .beanStore(beanStore) 078 .configurations(JettyConfiguration.class) 079 // This example has no interactive commands to offer; skip the Java console entirely so 080 // startup doesn't print "Could not create console command" for whatever default console 081 // commands a discovered config might otherwise register. 082 .consoleEnabled(false) 083 .build(); 084 microservice.start(); 085 086 return new ExampleServer(microservice, URI.create("http://localhost:" + boundPort(jetty) + "/")); 087 } 088 089 /** 090 * Returns the root URL the server is listening on (e.g. {@code http://localhost:10000/}). 091 * 092 * @return The root URL. Never <jk>null</jk>. 093 */ 094 public URI getRootUrl() { 095 return rootUrl; 096 } 097 098 @Override 099 public void close() throws Exception { 100 microservice.stop(); 101 } 102 103 /** 104 * Runs the server until the process is killed. 105 * 106 * @param args Optional single argument: the port to listen on (defaults to {@link #DEFAULT_PORT}). 107 * @throws Exception If the server fails to start. 108 */ 109 public static void main(String[] args) throws Exception { 110 var port = args.length > 0 ? Integer.parseInt(args[0]) : DEFAULT_PORT; 111 var server = start(port); 112 System.out.println("Juneau MCP example server is listening at " + server.getRootUrl()); 113 System.out.println("Drive it with: ExampleClient " + server.getRootUrl()); 114 System.out.println("Press Ctrl-C to stop."); 115 // Microservice.join() defaults to a documented no-op, so it won't actually block here; join the 116 // main thread directly to keep the process (and the Jetty daemon threads it started) alive. 117 Thread.currentThread().join(); 118 } 119 120 /** Builds a Jetty server with a single connector on {@code port} and a root servlet context. */ 121 private static Server buildServer(int port) { 122 var server = new Server(); 123 var connector = new ServerConnector(server); 124 connector.setPort(port); 125 server.addConnector(connector); 126 var handler = new ServletContextHandler(); 127 handler.setContextPath("/"); 128 // JettyServerComponent discovers the handler via this attribute (same convention as jetty.xml). 129 server.setAttribute("ServletContextHandler", handler); 130 server.setHandler(handler); 131 return server; 132 } 133 134 /** Reads the actual bound port (meaningful even when the server was started on port 0). */ 135 private static int boundPort(Server server) { 136 for (var connector : server.getConnectors()) 137 if (connector instanceof ServerConnector sc) 138 return sc.getLocalPort(); 139 throw new IllegalStateException("Could not determine the bound port of the Jetty server."); 140 } 141}