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 &mdash; 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}