Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mcp-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
<version>${project.version}</version>
</dependency>

<!-- MCP Server TCK (Technology Compatibility Kit) -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-tck-http</artifactId>
<version>${project.version}</version>
</dependency>

<!-- MCP Transport - WebFlux SSE -->
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2017-2025 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.modelcontextprotocol.server;

/**
* An HTTP MCP Server.
*/
public interface McpHttpServer extends AutoCloseable {

/**
* Starts the MCP Server.
*/
void start();

/**
* @return The Port the server is running at
*/
int getPort();

/**
* @return the MCP endpoint path. For example, `/mcp`
*/
String getEndpoint();

/**
* Returns the default {@link McpHttpServer}.
* @return The default {@link McpHttpServer}
* @throws IllegalStateException If no {@link McpHttpServer} implementation exists on
* the classpath.
*/
static McpHttpServer getDefault() {
return McpHttpServerInternal.getDefaultMapper();
}

/**
* Creates a new default {@link McpHttpServer}.
* @return The default {@link McpHttpServer}
* @throws IllegalStateException If no {@link McpHttpServer} implementation exists on
* the classpath.
*/
static McpHttpServer createDefault() {
return McpHttpServerInternal.createDefaultMapper();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2017-2025 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.modelcontextprotocol.server;

import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

/**
* Utility class for creating a default {@link McpHttpServer} instance. This can be used
* by TCK (Technology Compatibility Kit) suites. This class provides a single method to
* create a default mapper using the {@link ServiceLoader} mechanism.
*/
final class McpHttpServerInternal {

private static McpHttpServer defaultJsonMapper = null;

/**
* Returns the cached default {@link McpHttpServer} instance. If the default mapper
* has not been created yet, it will be initialized using the
* {@link #createDefaultMapper()} method.
* @return the default {@link McpHttpServer} instance
* @throws IllegalStateException if no default {@link McpHttpServer} implementation is
* found
*/
static McpHttpServer getDefaultMapper() {
if (defaultJsonMapper == null) {
defaultJsonMapper = McpHttpServerInternal.createDefaultMapper();
}
return defaultJsonMapper;
}

/**
* Creates a default {@link McpHttpServer} instance using the {@link ServiceLoader}
* mechanism. The default mapper is resolved by loading the first available
* {@link McpHttpServerSupplier} implementation on the classpath.
* @return the default {@link McpHttpServer} instance
* @throws IllegalStateException if no default {@link McpHttpServer} implementation is
* found
*/
static McpHttpServer createDefaultMapper() {
AtomicReference<IllegalStateException> ex = new AtomicReference<>();
return ServiceLoader.load(McpHttpServerSupplier.class).stream().flatMap(p -> {
try {
McpHttpServerSupplier supplier = p.get();
return Stream.ofNullable(supplier);
}
catch (Exception e) {
addException(ex, e);
return Stream.empty();
}
}).flatMap(jsonMapperSupplier -> {
try {
return Stream.ofNullable(jsonMapperSupplier.get());
}
catch (Exception e) {
addException(ex, e);
return Stream.empty();
}
}).findFirst().orElseThrow(() -> {
if (ex.get() != null) {
return ex.get();
}
else {
return new IllegalStateException("No default McpHttpServer implementation found");
}
});
}

private static void addException(AtomicReference<IllegalStateException> ref, Exception toAdd) {
ref.updateAndGet(existing -> {
if (existing == null) {
return new IllegalStateException("Failed to initialize default McpHttpServer", toAdd);
}
else {
existing.addSuppressed(toAdd);
return existing;
}
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2017-2025 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.modelcontextprotocol.server;

import java.util.function.Supplier;

/**
* Strategy interface for resolving a {@link McpHttpServer}.
*/
public interface McpHttpServerSupplier extends Supplier<McpHttpServer> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright 2017-2025 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.modelcontextprotocol.server.httpserver;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import io.modelcontextprotocol.server.transport.HttpJsonRpcResponse;
import io.modelcontextprotocol.server.transport.HttpServerMcpStatelessServerTransport;
import io.modelcontextprotocol.server.McpHttpServer;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Map;

/**
* Mcp HTTP Server class which uses a {@link HttpServer}.
*/
public class McpSimpleHttpServer implements McpHttpServer {

private static final Logger LOG = LoggerFactory.getLogger(McpSimpleHttpServer.class);

private static final String METHOD_POST = "POST";

private static final String HEADER_CONTENT_TYPE = "Content-Type";

private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";

private static final String DEFAULT_ENDPOINT = "/mcp";

protected final HttpServer server;

protected final String endpoint;

protected final McpJsonMapper jsonMapper;

protected final HttpServerMcpStatelessServerTransport<HttpExchange> transport;

/**
* @param transport Transport
* @throws IOException IO Exception while invoking
* {@link HttpServer#create(InetSocketAddress, int)}
*/
public McpSimpleHttpServer(HttpServerMcpStatelessServerTransport<HttpExchange> transport) throws IOException {
this(new InetSocketAddress(0), DEFAULT_ENDPOINT, transport, McpJsonMapper.getDefault());
}

/**
* @param transport Transport
* @param jsonMapper JSON Mapper
* @throws IOException IO Exception while invoking
* {@link HttpServer#create(InetSocketAddress, int)}
*/
public McpSimpleHttpServer(HttpServerMcpStatelessServerTransport<HttpExchange> transport, McpJsonMapper jsonMapper)
throws IOException {
this(new InetSocketAddress(0), DEFAULT_ENDPOINT, transport, jsonMapper);
}

/**
* @param inetSocketAddress address
* @param endpoint endpoint
* @param transport transport
* @param jsonMapper JSON Mapper
* @throws IOException IO Exception while invoking
* {@link HttpServer#create(InetSocketAddress, int)}
*/
public McpSimpleHttpServer(InetSocketAddress inetSocketAddress, String endpoint,
HttpServerMcpStatelessServerTransport<HttpExchange> transport, McpJsonMapper jsonMapper)
throws IOException {
this.endpoint = endpoint;
this.jsonMapper = jsonMapper;
this.transport = transport;
this.server = HttpServer.create(inetSocketAddress, 0);
server.createContext(endpoint, createHttpHandler());
}

@Override
public int getPort() {
return getAddress().getPort();
}

@Override
public void start() {
server.start();
}

@Override
public String getEndpoint() {
return this.endpoint;
}

@Override
public void close() {
stop();
}

/**
* Stop this server.
*/
public void stop() {
server.stop(0);
}

/**
* Stop this server.
* @param delay the maximum time in seconds to wait until exchanges have finished
*/
public void stop(int delay) {
server.stop(delay);
}

private HttpHandler createHttpHandler() {
return exchange -> {
try {
if (exchange.getRequestMethod().equalsIgnoreCase(METHOD_POST)) {
if (hasJsonContentType(exchange)) {
Map<String, Object> body = body(exchange);
HttpJsonRpcResponse rsp = transport.handlePost(exchange, body).block();
sendResponse(rsp, exchange);
}
else {
exchange.sendResponseHeaders(422, -1);
}
}
else {
exchange.sendResponseHeaders(405, -1);
}
}
catch (IOException e) {
if (LOG.isErrorEnabled()) {
LOG.error(e.getMessage(), e);
}
}
finally {
exchange.close();
}
};
}

private void sendResponse(HttpJsonRpcResponse rsp, HttpExchange exchange) throws IOException {
if (rsp == null || rsp.body() == null) {
exchange.sendResponseHeaders(rsp != null ? rsp.statusCode() : 202, -1);
return;
}
byte[] responseBytes = jsonMapper.writeValueAsBytes(rsp.body());
exchange.getResponseHeaders().add(HEADER_CONTENT_TYPE, MEDIA_TYPE_APPLICATION_JSON);
exchange.sendResponseHeaders(rsp.statusCode(), responseBytes.length);
exchange.getResponseBody().write(responseBytes);
}

private boolean hasJsonContentType(HttpExchange exchange) {
return exchange.getRequestHeaders().containsKey(HEADER_CONTENT_TYPE)
&& exchange.getRequestHeaders().getFirst(HEADER_CONTENT_TYPE).equals(MEDIA_TYPE_APPLICATION_JSON);
}

private Map<String, Object> body(HttpExchange exchange) throws IOException {
TypeRef<Map<String, Object>> typeRef = new TypeRef<>() {

};
byte[] requestBytes = exchange.getRequestBody().readAllBytes();
return jsonMapper.readValue(requestBytes, typeRef);
}

private InetSocketAddress getAddress() {
return server.getAddress();
}

}
Loading