You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by bt...@apache.org on 2026/09/15 13:23:37 UTC

(hadoop) branch trunk updated: YARN-11977. Add a Hadoop-native MCP HTTP server framework in hadoop-common (#8665)

This is an automated email from the ASF dual-hosted git repository.

brumi1024 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/hadoop.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 90b043bd820 YARN-11977. Add a Hadoop-native MCP HTTP server framework in hadoop-common (#8665)
90b043bd820 is described below

commit 90b043bd820a840e1393f73c0dfdc2b521f7680c
Author: K0K0V0K <10...@users.noreply.github.com>
AuthorDate: Tue Sep 15 15:23:29 2026 +0200

    YARN-11977. Add a Hadoop-native MCP HTTP server framework in hadoop-common (#8665)
    
    Introduce org.apache.hadoop.mcp with JSON-RPC streamable HTTP transport and McpHttpServer/McpServer.
---
 .../apache/hadoop/mcp/JacksonMcpJsonMapper.java    |  71 +++++
 .../java/org/apache/hadoop/mcp/McpCallContext.java |  42 +++
 .../org/apache/hadoop/mcp/McpHttpResponse.java     |  68 +++++
 .../java/org/apache/hadoop/mcp/McpHttpServer.java  | 277 +++++++++++++++++++
 .../java/org/apache/hadoop/mcp/McpHttpServlet.java | 111 ++++++++
 .../java/org/apache/hadoop/mcp/McpJsonMapper.java  |  41 +++
 .../java/org/apache/hadoop/mcp/McpJsonRpc.java     |  54 ++++
 .../org/apache/hadoop/mcp/McpJsonRpcResponses.java |  95 +++++++
 .../org/apache/hadoop/mcp/McpJsonRpcValidator.java |  89 ++++++
 .../org/apache/hadoop/mcp/McpRequestHandler.java   | 190 +++++++++++++
 .../main/java/org/apache/hadoop/mcp/McpSchema.java | 147 ++++++++++
 .../main/java/org/apache/hadoop/mcp/McpServer.java | 139 ++++++++++
 .../org/apache/hadoop/mcp/McpToolCallHandler.java  |  36 +++
 .../java/org/apache/hadoop/mcp/McpToolSchema.java  |  86 ++++++
 .../java/org/apache/hadoop/mcp/package-info.java   |  27 ++
 .../org/apache/hadoop/mcp/TestMcpHttpServer.java   | 217 +++++++++++++++
 .../org/apache/hadoop/mcp/TestMcpHttpServlet.java  | 303 +++++++++++++++++++++
 .../apache/hadoop/mcp/TestMcpJsonRpcValidator.java | 176 ++++++++++++
 .../org/apache/hadoop/mcp/TestMcpToolSchema.java   |  62 +++++
 19 files changed, 2231 insertions(+)

diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/JacksonMcpJsonMapper.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/JacksonMcpJsonMapper.java
new file mode 100644
index 00000000000..7e260242bce
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/JacksonMcpJsonMapper.java
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Jackson-backed {@link McpJsonMapper}.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class JacksonMcpJsonMapper implements McpJsonMapper {
+
+  private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
+
+  private final ObjectMapper mapper;
+
+  public JacksonMcpJsonMapper() {
+    ObjectMapper objectMapper = new ObjectMapper();
+    objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
+    this.mapper = objectMapper;
+  }
+
+  public JacksonMcpJsonMapper(ObjectMapper mapper) {
+    this.mapper = mapper;
+  }
+
+  public ObjectMapper getObjectMapper() {
+    return mapper;
+  }
+
+  @Override
+  public String writeValueAsString(Object value) throws IOException {
+    return mapper.writeValueAsString(value);
+  }
+
+  @Override
+  public JsonNode readTree(String json) throws IOException {
+    return mapper.readTree(json);
+  }
+
+  @Override
+  public Map<String, Object> readMap(String json) throws IOException {
+    return mapper.readValue(json, MAP_TYPE);
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpCallContext.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpCallContext.java
new file mode 100644
index 00000000000..03ff6d4c30c
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpCallContext.java
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+/**
+ * Request-scoped context passed to MCP tool handlers.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpCallContext {
+
+  private final HttpServletRequest request;
+
+  public McpCallContext(HttpServletRequest request) {
+    this.request = request;
+  }
+
+  public HttpServletRequest getRequest() {
+    return request;
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpResponse.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpResponse.java
new file mode 100644
index 00000000000..a2d41afc09f
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpResponse.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * HTTP response produced by {@link McpRequestHandler}.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpHttpResponse {
+
+  private static final int STATUS_OK = 200;
+  private static final int STATUS_ACCEPTED = 202;
+
+  private final int status;
+  private final Map<String, String> headers;
+  private final JsonNode body;
+
+  private McpHttpResponse(int status, Map<String, String> headers, JsonNode body) {
+    this.status = status;
+    this.headers = headers;
+    this.body = body;
+  }
+
+  public static McpHttpResponse notification() {
+    return new McpHttpResponse(STATUS_ACCEPTED, Collections.emptyMap(), null);
+  }
+
+  public static McpHttpResponse ok(JsonNode body, Map<String, String> headers) {
+    return new McpHttpResponse(STATUS_OK, headers, body);
+  }
+
+  public int status() {
+    return status;
+  }
+
+  public Map<String, String> headers() {
+    return headers;
+  }
+
+  public JsonNode body() {
+    return body;
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServer.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServer.java
new file mode 100644
index 00000000000..102c74c4304
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServer.java
@@ -0,0 +1,277 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.security.GeneralSecurityException;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.http.HttpServer2;
+import org.apache.hadoop.security.ssl.SSLFactory;
+import org.apache.hadoop.util.StringUtils;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Minimal Jetty HTTP/HTTPS server that hosts an {@link McpServer} servlet on a dedicated port.
+ *
+ * <p>No Kerberos/SPNEGO filters are installed. Callers authenticate MCP tool calls
+ * at the application layer (for example API keys).
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpHttpServer implements AutoCloseable {
+
+  private static final Logger LOG = LoggerFactory.getLogger(McpHttpServer.class);
+
+  private final McpServer mcpServer;
+  private final Server server;
+  private final ServerConnector connector;
+  private final SSLFactory sslFactory;
+
+  private McpHttpServer(McpServer mcpServer, Server server, ServerConnector connector,
+      SSLFactory sslFactory) {
+    this.mcpServer = mcpServer;
+    this.server = server;
+    this.connector = connector;
+    this.sslFactory = sslFactory;
+  }
+
+  /**
+   * Starts a dedicated MCP HTTPS server.
+   *
+   * @param mcpServer MCP server whose servlet handles JSON-RPC requests
+   * @param conf daemon configuration containing SSL keystore settings
+   * @param bindAddress address to bind (port {@code 0} picks an ephemeral port)
+   * @param pathSpec servlet path, for example {@code /ws/v1/mcp}
+   */
+  public static McpHttpServer start(McpServer mcpServer, Configuration conf,
+      InetSocketAddress bindAddress, String pathSpec) throws IOException {
+    return start(mcpServer, conf, bindAddress, pathSpec, true);
+  }
+
+  /**
+   * Starts a dedicated MCP HTTP or HTTPS server.
+   *
+   * @param mcpServer MCP server whose servlet handles JSON-RPC requests
+   * @param conf daemon configuration; SSL keystore settings are required when {@code useHttps}
+   *     is {@code true}
+   * @param bindAddress address to bind (port {@code 0} picks an ephemeral port)
+   * @param pathSpec servlet path, for example {@code /ws/v1/mcp}
+   * @param useHttps when {@code true}, serve HTTPS using the daemon SSL keystore; otherwise
+   *     serve plain HTTP
+   */
+  public static McpHttpServer start(McpServer mcpServer, Configuration conf,
+      InetSocketAddress bindAddress, String pathSpec, boolean useHttps) throws IOException {
+    Configuration serverConf = conf == null ? new Configuration(false) : conf;
+    SSLFactory sslFactory = null;
+    Server server = new Server();
+    ServerConnector connector;
+    if (useHttps) {
+      sslFactory = new SSLFactory(SSLFactory.Mode.SERVER, serverConf);
+      try {
+        sslFactory.init();
+      } catch (GeneralSecurityException e) {
+        throw new IOException("Failed to initialize SSLFactory", e);
+      }
+      connector = createHttpsConnector(server, serverConf, bindAddress, sslFactory);
+    } else {
+      connector = createHttpConnector(server, bindAddress);
+    }
+    server.addConnector(connector);
+
+    ServletContextHandler context =
+        new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+    context.setContextPath("/");
+    context.addServlet(new ServletHolder(mcpServer.getServlet()), pathSpec);
+    server.setHandler(context);
+
+    try {
+      server.start();
+    } catch (Exception e) {
+      if (sslFactory != null) {
+        sslFactory.destroy();
+      }
+      try {
+        server.stop();
+      } catch (Exception ex) {
+        LOG.debug("Failed to stop server after start failed", ex);
+      }
+      throw new IOException("Failed to start MCP HTTP server", e);
+    }
+
+    String host = bindAddress.getHostString();
+    if (host == null || host.isEmpty()) {
+      host = "0.0.0.0";
+    }
+    String scheme = useHttps ? "https" : "http";
+    LOG.info("MCP {} server listening on {}://{}:{}{}",
+        useHttps ? "HTTPS" : "HTTP", scheme, host, connector.getLocalPort(), pathSpec);
+    return new McpHttpServer(mcpServer, server, connector, sslFactory);
+  }
+
+  private static ServerConnector createHttpConnector(Server server,
+      InetSocketAddress bindAddress) {
+    HttpConfiguration httpConfig = new HttpConfiguration();
+    httpConfig.setSendServerVersion(false);
+
+    ServerConnector connector = new ServerConnector(server,
+        new HttpConnectionFactory(httpConfig));
+    applyBindAddress(connector, bindAddress);
+    return connector;
+  }
+
+  private static ServerConnector createHttpsConnector(Server server, Configuration sslConf,
+      InetSocketAddress bindAddress, SSLFactory sslFactory) throws IOException {
+    HttpConfiguration httpConfig = new HttpConfiguration();
+    httpConfig.setSendServerVersion(false);
+    httpConfig.setSecureScheme("https");
+    boolean sniHostCheckEnabled = sslConf.getBoolean(
+        HttpServer2.HTTP_SNI_HOST_CHECK_ENABLED_KEY,
+        HttpServer2.HTTP_SNI_HOST_CHECK_ENABLED_DEFAULT);
+    httpConfig.addCustomizer(new SecureRequestCustomizer(sniHostCheckEnabled));
+
+    ServerConnector connector = new ServerConnector(server);
+    SslContextFactory.Server sslContextFactory =
+        createJettySslContextFactory(sslFactory, sslConf);
+    connector.addConnectionFactory(new HttpConnectionFactory(httpConfig));
+    connector.addFirstConnectionFactory(new SslConnectionFactory(sslContextFactory,
+        HttpVersion.HTTP_1_1.asString()));
+
+    applyBindAddress(connector, bindAddress);
+    return connector;
+  }
+
+  private static void applyBindAddress(ServerConnector connector,
+      InetSocketAddress bindAddress) {
+    String host = bindAddress.getHostString();
+    if (host != null && !host.isEmpty() && !"0.0.0.0".equals(host)) {
+      connector.setHost(host);
+    }
+    connector.setPort(bindAddress.getPort());
+  }
+
+  private static SslContextFactory.Server createJettySslContextFactory(
+      SSLFactory sslFactory, Configuration conf) throws IOException {
+    Configuration sslServerConf =
+        SSLFactory.readSSLConfiguration(conf, SSLFactory.Mode.SERVER);
+    String keyStore = sslServerConf.getTrimmed(SSLFactory.SSL_SERVER_KEYSTORE_LOCATION);
+    if (keyStore == null || keyStore.isEmpty()) {
+      throw new IOException("Property " + SSLFactory.SSL_SERVER_KEYSTORE_LOCATION
+          + " not specified");
+    }
+
+    SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
+    sslContextFactory.setKeyStorePath(keyStore);
+    sslContextFactory.setKeyStoreType(sslServerConf.get(SSLFactory.SSL_SERVER_KEYSTORE_TYPE,
+        SSLFactory.SSL_SERVER_KEYSTORE_TYPE_DEFAULT));
+    String keyStorePassword = getPasswordString(sslServerConf,
+        SSLFactory.SSL_SERVER_KEYSTORE_PASSWORD);
+    if (keyStorePassword == null) {
+      throw new IOException("Property " + SSLFactory.SSL_SERVER_KEYSTORE_PASSWORD
+          + " not specified");
+    }
+    sslContextFactory.setKeyStorePassword(keyStorePassword);
+
+    String keyPassword = getPasswordString(sslServerConf,
+        SSLFactory.SSL_SERVER_KEYSTORE_KEYPASSWORD);
+    if (keyPassword != null) {
+      sslContextFactory.setKeyManagerPassword(keyPassword);
+    }
+
+    String trustStore = sslServerConf.get(SSLFactory.SSL_SERVER_TRUSTSTORE_LOCATION);
+    if (trustStore != null) {
+      sslContextFactory.setTrustStorePath(trustStore);
+      sslContextFactory.setTrustStoreType(sslServerConf.get(
+          SSLFactory.SSL_SERVER_TRUSTSTORE_TYPE,
+          SSLFactory.SSL_SERVER_TRUSTSTORE_TYPE_DEFAULT));
+      String trustStorePassword = getPasswordString(sslServerConf,
+          SSLFactory.SSL_SERVER_TRUSTSTORE_PASSWORD);
+      if (trustStorePassword != null) {
+        sslContextFactory.setTrustStorePassword(trustStorePassword);
+      }
+    }
+
+    sslContextFactory.setNeedClientAuth(sslFactory.isClientCertRequired());
+
+    String[] enabledProtocols = conf.getStrings(SSLFactory.SSL_ENABLED_PROTOCOLS_KEY,
+        SSLFactory.SSL_ENABLED_PROTOCOLS_DEFAULT);
+    if (enabledProtocols != null) {
+      sslContextFactory.setIncludeProtocols(enabledProtocols);
+    }
+
+    String excludeCiphers = sslServerConf.get(SSLFactory.SSL_SERVER_EXCLUDE_CIPHER_LIST);
+    if (StringUtils.hasLength(excludeCiphers)) {
+      sslContextFactory.setExcludeCipherSuites(StringUtils.getTrimmedStrings(excludeCiphers));
+    }
+    String includeCiphers = sslServerConf.get(SSLFactory.SSL_SERVER_INCLUDE_CIPHER_LIST);
+    if (StringUtils.hasLength(includeCiphers)) {
+      sslContextFactory.setIncludeCipherSuites(StringUtils.getTrimmedStrings(includeCiphers));
+    }
+    return sslContextFactory;
+  }
+
+  private static String getPasswordString(Configuration conf, String name)
+      throws IOException {
+    char[] password = conf.getPassword(name);
+    if (password == null) {
+      return null;
+    }
+    return new String(password);
+  }
+
+  /** Local port of the connector (useful when bind port was 0). */
+  public int getPort() {
+    return connector.getLocalPort();
+  }
+
+  public InetSocketAddress getConnectorAddress() {
+    String host = connector.getHost();
+    if (host == null || host.isEmpty()) {
+      host = "0.0.0.0";
+    }
+    return new InetSocketAddress(host, connector.getLocalPort());
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      server.stop();
+    } catch (Exception e) {
+      throw new IOException("Failed to stop MCP HTTP server", e);
+    }
+    if (sslFactory != null) {
+      sslFactory.destroy();
+    }
+    mcpServer.close();
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServlet.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServlet.java
new file mode 100644
index 00000000000..f6cfd083a9c
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpHttpServlet.java
@@ -0,0 +1,111 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * javax.servlet endpoint implementing MCP streamable HTTP transport.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpHttpServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 1L;
+
+  private final ObjectMapper objectMapper;
+  private final McpRequestHandler requestHandler;
+
+  McpHttpServlet(ObjectMapper objectMapper, McpRequestHandler requestHandler) {
+    this.objectMapper = objectMapper;
+    this.requestHandler = requestHandler;
+  }
+
+  void close() {
+    // Stateless server; nothing to release.
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    req.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
+
+    byte[] body = readRequestBody(req);
+    if (body.length == 0 || body.length > McpJsonRpc.MAX_REQUEST_BODY_BYTES) {
+      writeResponse(objectMapper, resp, body.length == 0
+          ? requestHandler.parseErrorResponse()
+          : requestHandler.requestBodyTooLargeResponse());
+      return;
+    }
+
+    JsonNode requestNode;
+    try {
+      requestNode = objectMapper.readTree(body);
+    } catch (JsonProcessingException e) {
+      writeResponse(objectMapper, resp, requestHandler.parseErrorResponse());
+      return;
+    }
+
+    writeResponse(objectMapper, resp,
+        requestHandler.handle(requestNode, new McpCallContext(req)));
+  }
+
+  private byte[] readRequestBody(HttpServletRequest req) throws IOException {
+    try (InputStream in = new BoundedInputStream(req.getInputStream(),
+        McpJsonRpc.MAX_REQUEST_BODY_BYTES + 1L)) {
+      return IOUtils.toByteArray(in);
+    }
+  }
+
+  static void writeResponse(ObjectMapper objectMapper, HttpServletResponse resp,
+      McpHttpResponse mcpResponse) throws IOException {
+    for (Map.Entry<String, String> header : mcpResponse.headers().entrySet()) {
+      resp.setHeader(header.getKey(), header.getValue());
+    }
+    resp.setStatus(mcpResponse.status());
+    if (mcpResponse.body() != null) {
+      resp.setContentType("application/json");
+      objectMapper.writeValue(resp.getWriter(), mcpResponse.body());
+    }
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonMapper.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonMapper.java
new file mode 100644
index 00000000000..91cd096bb7c
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonMapper.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * JSON serialization support for MCP messages.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public interface McpJsonMapper {
+
+  String writeValueAsString(Object value) throws IOException;
+
+  JsonNode readTree(String json) throws IOException;
+
+  Map<String, Object> readMap(String json) throws IOException;
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpc.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpc.java
new file mode 100644
index 00000000000..d13882e636d
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpc.java
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+/**
+ * JSON-RPC 2.0 constants used by the MCP HTTP transport.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpJsonRpc {
+
+  public static final String VERSION = "2.0";
+
+  /** MCP protocol version implemented by this server. */
+  public static final String PROTOCOL_VERSION = "2025-06-18";
+
+  /** JSON-RPC 2.0 parse error. */
+  public static final int PARSE_ERROR = -32700;
+  /** JSON-RPC 2.0 invalid request. */
+  public static final int INVALID_REQUEST = -32600;
+  /** JSON-RPC 2.0 method not found. */
+  public static final int METHOD_NOT_FOUND = -32601;
+  /** JSON-RPC 2.0 invalid params. */
+  public static final int INVALID_PARAMS = -32602;
+
+  public static final String PARSE_ERROR_MESSAGE = "Parse error";
+  public static final String INVALID_REQUEST_MESSAGE = "Invalid Request";
+  public static final String REQUEST_BODY_TOO_LARGE_MESSAGE = "Request body too large";
+
+  /** Maximum MCP JSON-RPC request body size accepted by {@link McpHttpServlet}. */
+  public static final int MAX_REQUEST_BODY_BYTES = 1024 * 1024;
+
+  private McpJsonRpc() {
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcResponses.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcResponses.java
new file mode 100644
index 00000000000..007556f446f
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcResponses.java
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.mcp.McpSchema.CallToolResult;
+import org.apache.hadoop.mcp.McpSchema.TextContent;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Builds JSON-RPC 2.0 HTTP responses for MCP.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+final class McpJsonRpcResponses {
+
+  private final ObjectMapper objectMapper;
+
+  McpJsonRpcResponses(ObjectMapper objectMapper) {
+    this.objectMapper = objectMapper;
+  }
+
+  McpHttpResponse success(JsonNode idNode, Object result) {
+    return success(idNode, result, Collections.emptyMap());
+  }
+
+  McpHttpResponse success(JsonNode idNode, Object result, Map<String, String> headers) {
+    ObjectNode response = objectMapper.createObjectNode();
+    response.put("jsonrpc", McpJsonRpc.VERSION);
+    if (idNode != null && !idNode.isNull()) {
+      response.set("id", idNode);
+    }
+    response.set("result", objectMapper.valueToTree(result));
+    return McpHttpResponse.ok(response, headers);
+  }
+
+  McpHttpResponse error(JsonNode idNode, int code, String message) {
+    ObjectNode response = objectMapper.createObjectNode();
+    response.put("jsonrpc", McpJsonRpc.VERSION);
+    setErrorResponseId(response, idNode);
+    ObjectNode error = objectMapper.createObjectNode();
+    error.put("code", code);
+    error.put("message", message);
+    response.set("error", error);
+    return McpHttpResponse.ok(response, Collections.emptyMap());
+  }
+
+  private void setErrorResponseId(ObjectNode response, JsonNode idNode) {
+    if (idNode != null && !idNode.isNull()) {
+      response.set("id", idNode);
+    } else {
+      response.putNull("id");
+    }
+  }
+
+  static Map<String, Object> toResultMap(CallToolResult callResult) {
+    List<Map<String, Object>> content = new ArrayList<>();
+    for (TextContent textContent : callResult.content()) {
+      Map<String, Object> item = new LinkedHashMap<>();
+      item.put("type", textContent.type());
+      item.put("text", textContent.text());
+      content.add(item);
+    }
+    Map<String, Object> result = new LinkedHashMap<>();
+    result.put("content", content);
+    result.put("isError", callResult.isError());
+    return result;
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcValidator.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcValidator.java
new file mode 100644
index 00000000000..9baae75dcec
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpJsonRpcValidator.java
@@ -0,0 +1,89 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Validates JSON-RPC envelope fields required by MCP 2025-06-18.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+final class McpJsonRpcValidator {
+
+  private McpJsonRpcValidator() {
+  }
+
+  /**
+   * @return an error response when the envelope is invalid, otherwise {@code null}
+   */
+  static McpHttpResponse validate(JsonNode requestNode, McpJsonRpcResponses responses) {
+    if (requestNode == null || requestNode.isNull() || !requestNode.isObject()) {
+      return responses.error(null, McpJsonRpc.INVALID_REQUEST, McpJsonRpc.INVALID_REQUEST_MESSAGE);
+    }
+
+    JsonNode jsonrpcNode = requestNode.get("jsonrpc");
+    if (jsonrpcNode == null || jsonrpcNode.isNull()
+        || !jsonrpcNode.isTextual()
+        || !McpJsonRpc.VERSION.equals(jsonrpcNode.asText())) {
+      return responses.error(responseId(requestNode), McpJsonRpc.INVALID_REQUEST,
+          McpJsonRpc.INVALID_REQUEST_MESSAGE);
+    }
+
+    JsonNode methodNode = requestNode.get("method");
+    if (methodNode == null || methodNode.isNull() || !methodNode.isTextual()
+        || methodNode.asText().isEmpty()) {
+      return responses.error(responseId(requestNode), McpJsonRpc.INVALID_REQUEST,
+          McpJsonRpc.INVALID_REQUEST_MESSAGE);
+    }
+
+    JsonNode paramsNode = requestNode.get("params");
+    if (paramsNode != null && paramsNode.isNull()) {
+      return responses.error(responseId(requestNode), McpJsonRpc.INVALID_REQUEST,
+          McpJsonRpc.INVALID_REQUEST_MESSAGE);
+    }
+
+    if (isJsonRpcNotification(requestNode)) {
+      return null;
+    }
+
+    if (!isValidRequestId(requestNode.get("id"))) {
+      return responses.error(null, McpJsonRpc.INVALID_REQUEST, McpJsonRpc.INVALID_REQUEST_MESSAGE);
+    }
+    return null;
+  }
+
+  static boolean isJsonRpcNotification(JsonNode requestNode) {
+    return !requestNode.has("id");
+  }
+
+  private static boolean isValidRequestId(JsonNode idNode) {
+    return idNode != null && !idNode.isNull()
+        && (idNode.isIntegralNumber() || idNode.isTextual());
+  }
+
+  /** Returns {@code id} when it is a valid request id, otherwise {@code null}. */
+  private static JsonNode responseId(JsonNode requestNode) {
+    JsonNode idNode = requestNode.get("id");
+    return isValidRequestId(idNode) ? idNode : null;
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpRequestHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpRequestHandler.java
new file mode 100644
index 00000000000..83d03578606
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpRequestHandler.java
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.mcp.McpSchema.ServerCapabilities;
+import org.apache.hadoop.mcp.McpSchema.Tool;
+import org.apache.hadoop.mcp.McpServer.RegisteredTool;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Transport-neutral MCP JSON-RPC request handler.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpRequestHandler {
+
+  public static final String SESSION_HEADER = "Mcp-Session-Id";
+
+  public static final String METHOD_INITIALIZE = "initialize";
+  public static final String METHOD_TOOLS_LIST = "tools/list";
+  public static final String METHOD_TOOLS_CALL = "tools/call";
+
+  private static final String TOOL_EXECUTION_FAILED_MESSAGE = "Tool execution failed";
+
+  private static final TypeReference<Map<String, Object>> PARAMS_TYPE = new TypeReference<>() {};
+
+  private final ObjectMapper objectMapper;
+  private final String serverName;
+  private final String serverVersion;
+  private final ServerCapabilities capabilities;
+  private final Map<String, RegisteredTool> tools;
+  private final McpJsonRpcResponses jsonRpcResponses;
+
+  McpRequestHandler(ObjectMapper objectMapper, String serverName, String serverVersion,
+      ServerCapabilities capabilities, Map<String, RegisteredTool> tools) {
+    this.objectMapper = objectMapper;
+    this.serverName = serverName;
+    this.serverVersion = serverVersion;
+    this.capabilities = capabilities;
+    this.tools = Collections.unmodifiableMap(new LinkedHashMap<>(tools));
+    this.jsonRpcResponses = new McpJsonRpcResponses(objectMapper);
+  }
+
+  public McpHttpResponse handle(JsonNode requestNode) {
+    return handle(requestNode, new McpCallContext(null));
+  }
+
+  public McpHttpResponse parseErrorResponse() {
+    return jsonRpcResponses.error(null, McpJsonRpc.PARSE_ERROR, McpJsonRpc.PARSE_ERROR_MESSAGE);
+  }
+
+  public McpHttpResponse requestBodyTooLargeResponse() {
+    return jsonRpcResponses.error(null, McpJsonRpc.INVALID_REQUEST,
+        McpJsonRpc.REQUEST_BODY_TOO_LARGE_MESSAGE);
+  }
+
+  public McpHttpResponse handle(JsonNode requestNode, McpCallContext context) {
+    McpHttpResponse error = McpJsonRpcValidator.validate(requestNode, jsonRpcResponses);
+    if (error != null) {
+      return error;
+    }
+
+    if (McpJsonRpcValidator.isJsonRpcNotification(requestNode)) {
+      return McpHttpResponse.notification();
+    }
+
+    String method = requestNode.get("method").asText();
+    JsonNode idNode = requestNode.get("id");
+    JsonNode paramsNode = requestNode.get("params");
+    if (paramsNode != null && !paramsNode.isNull() && !paramsNode.isObject()) {
+      return jsonRpcResponses.error(idNode, McpJsonRpc.INVALID_PARAMS,
+          "params must be a JSON object");
+    }
+    Map<String, Object> params = paramsNode == null
+        ? Collections.emptyMap()
+        : objectMapper.convertValue(paramsNode, PARAMS_TYPE);
+
+    switch (method) {
+    case METHOD_INITIALIZE:
+      return initializeResponse(idNode);
+    case METHOD_TOOLS_LIST:
+      return jsonRpcResponses.success(idNode, buildToolsListResult());
+    case METHOD_TOOLS_CALL:
+      return toolsCallResponse(idNode, params, context);
+    default:
+      return jsonRpcResponses.error(idNode, McpJsonRpc.METHOD_NOT_FOUND,
+          "Method not found: " + method);
+    }
+  }
+
+  private McpHttpResponse initializeResponse(JsonNode idNode) {
+    Map<String, Object> result = new HashMap<>();
+    result.put("protocolVersion", McpJsonRpc.PROTOCOL_VERSION);
+    result.put("capabilities", capabilities.toMap());
+    Map<String, Object> serverInfo = new HashMap<>();
+    serverInfo.put("name", serverName);
+    serverInfo.put("version", serverVersion);
+    result.put("serverInfo", serverInfo);
+
+    Map<String, String> headers = new HashMap<>();
+    headers.put(SESSION_HEADER, UUID.randomUUID().toString());
+    return jsonRpcResponses.success(idNode, result, headers);
+  }
+
+  private Map<String, Object> buildToolsListResult() {
+    List<Map<String, Object>> toolList = new ArrayList<>();
+    for (RegisteredTool registeredTool : tools.values()) {
+      Tool tool = registeredTool.tool();
+      Map<String, Object> toolMap = new LinkedHashMap<>();
+      toolMap.put("name", tool.name());
+      toolMap.put("description", tool.description());
+      toolMap.put("inputSchema", tool.inputSchema());
+      toolList.add(toolMap);
+    }
+    Map<String, Object> result = new HashMap<>();
+    result.put("tools", toolList);
+    return result;
+  }
+
+  private McpHttpResponse toolsCallResponse(JsonNode idNode, Map<String, Object> params,
+      McpCallContext context) {
+    Object nameObject = params.get("name");
+    if (!(nameObject instanceof String)) {
+      return jsonRpcResponses.error(idNode, McpJsonRpc.INVALID_PARAMS, "Missing tool name");
+    }
+
+    RegisteredTool registeredTool = tools.get(nameObject);
+    if (registeredTool == null) {
+      return jsonRpcResponses.error(idNode, McpJsonRpc.INVALID_PARAMS,
+          "Unknown tool: " + nameObject);
+    }
+
+    Object argumentsObject = params.get("arguments");
+    Map<String, Object> arguments;
+    if (!params.containsKey("arguments")) {
+      arguments = Collections.emptyMap();
+    } else if (argumentsObject == null) {
+      return jsonRpcResponses.error(idNode, McpJsonRpc.INVALID_PARAMS,
+          "arguments must be a JSON object");
+    } else if (!(argumentsObject instanceof Map)) {
+      return jsonRpcResponses.error(idNode, McpJsonRpc.INVALID_PARAMS,
+          "arguments must be a JSON object");
+    } else {
+      arguments = objectMapper.convertValue(
+          objectMapper.valueToTree(argumentsObject), PARAMS_TYPE);
+    }
+
+    final McpSchema.CallToolResult callResult;
+    try {
+      callResult = registeredTool.call(context, arguments);
+    } catch (Exception e) {
+      String message = e.getMessage();
+      if (message == null || message.isEmpty()) {
+        message = TOOL_EXECUTION_FAILED_MESSAGE;
+      }
+      return jsonRpcResponses.success(idNode, McpJsonRpcResponses.toResultMap(
+          McpSchema.CallToolResult.error(message)));
+    }
+    return jsonRpcResponses.success(idNode, McpJsonRpcResponses.toResultMap(callResult));
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpSchema.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpSchema.java
new file mode 100644
index 00000000000..26ffe707387
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpSchema.java
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+/**
+ * MCP protocol data types.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpSchema {
+
+  private McpSchema() {
+  }
+
+  public static final class Tool {
+    private final String name;
+    private final String description;
+    private final Map<String, Object> inputSchema;
+
+    private Tool(String name, String description, Map<String, Object> inputSchema) {
+      this.name = name;
+      this.description = description;
+      this.inputSchema = inputSchema;
+    }
+
+    public String name() {
+      return name;
+    }
+
+    public String description() {
+      return description;
+    }
+
+    public Map<String, Object> inputSchema() {
+      return inputSchema;
+    }
+
+    public static Tool of(String name, String description, Map<String, Object> inputSchema) {
+      return new Tool(name, description, inputSchema);
+    }
+
+    public static Tool of(String name, String description, McpJsonMapper jsonMapper,
+        String schemaJson) {
+      try {
+        return of(name, description, jsonMapper.readMap(schemaJson));
+      } catch (IOException e) {
+        throw new UncheckedIOException("Failed to parse tool input schema", e);
+      }
+    }
+  }
+
+  public static final class TextContent {
+    private final String text;
+
+    public TextContent(String text) {
+      this.text = text;
+    }
+
+    public String type() {
+      return "text";
+    }
+
+    public String text() {
+      return text;
+    }
+  }
+
+  public static final class CallToolResult {
+    private final List<TextContent> content;
+    private final boolean isError;
+
+    private CallToolResult(List<TextContent> content, boolean isError) {
+      this.content = content;
+      this.isError = isError;
+    }
+
+    public List<TextContent> content() {
+      return content;
+    }
+
+    public boolean isError() {
+      return isError;
+    }
+
+    public static CallToolResult text(String text) {
+      return new CallToolResult(Collections.singletonList(new TextContent(text)), false);
+    }
+
+    public static CallToolResult error(String message) {
+      return new CallToolResult(Collections.singletonList(new TextContent(message)), true);
+    }
+  }
+
+  public static final class ServerCapabilities {
+    private final boolean tools;
+
+    private ServerCapabilities(boolean tools) {
+      this.tools = tools;
+    }
+
+    public boolean tools() {
+      return tools;
+    }
+
+    public Map<String, Object> toMap() {
+      Map<String, Object> capabilities = new HashMap<>();
+      if (tools) {
+        capabilities.put("tools", Collections.emptyMap());
+      }
+      return capabilities;
+    }
+
+    public static ServerCapabilities withTools() {
+      return new ServerCapabilities(true);
+    }
+
+    public static ServerCapabilities withoutTools() {
+      return new ServerCapabilities(false);
+    }
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpServer.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpServer.java
new file mode 100644
index 00000000000..95b5cef56d7
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpServer.java
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.mcp.McpSchema.CallToolResult;
+import org.apache.hadoop.mcp.McpSchema.ServerCapabilities;
+import org.apache.hadoop.mcp.McpSchema.Tool;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Minimal MCP server for streamable HTTP transport.
+ *
+ * <p>Host {@link #getServlet()} with {@link McpHttpServer} on a dedicated port:
+ * <pre>{@code
+ * McpServer mcpServer = McpServer.sync(jsonMapper)
+ *     .serverInfo("yarn-resourcemanager", version)
+ *     .capabilities(ServerCapabilities.withTools())
+ *     .toolCall(tool, handler)
+ *     .build();
+ * try (McpHttpServer httpServer = McpHttpServer.start(mcpServer, conf, bindAddress,
+ *     "/ws/v1/mcp")) {
+ *   // ...
+ * }
+ * }</pre>
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpServer implements Closeable {
+
+  private final McpRequestHandler requestHandler;
+  private final McpHttpServlet servlet;
+
+  private McpServer(McpRequestHandler requestHandler, McpHttpServlet servlet) {
+    this.requestHandler = requestHandler;
+    this.servlet = servlet;
+  }
+
+  public static Builder sync(McpJsonMapper jsonMapper) {
+    return new Builder(jsonMapper);
+  }
+
+  /**
+   * Returns a servlet suitable for {@code WebAppContext.addServlet(...)}.
+   */
+  public HttpServlet getServlet() {
+    return servlet;
+  }
+
+  McpRequestHandler getRequestHandler() {
+    return requestHandler;
+  }
+
+  @Override
+  public void close() throws IOException {
+    servlet.close();
+  }
+
+  public static final class Builder {
+    private final ObjectMapper objectMapper;
+    private String serverName = "hadoop-mcp";
+    private String serverVersion = "1.0.0";
+    private ServerCapabilities capabilities = ServerCapabilities.withoutTools();
+    private final Map<String, RegisteredTool> tools = new LinkedHashMap<>();
+
+    private Builder(McpJsonMapper jsonMapper) {
+      if (!(jsonMapper instanceof JacksonMcpJsonMapper)) {
+        throw new IllegalArgumentException("McpServer requires JacksonMcpJsonMapper");
+      }
+      this.objectMapper = ((JacksonMcpJsonMapper) jsonMapper).getObjectMapper();
+    }
+
+    public Builder serverInfo(String name, String version) {
+      this.serverName = name;
+      this.serverVersion = version;
+      return this;
+    }
+
+    public Builder capabilities(ServerCapabilities serverCapabilities) {
+      this.capabilities = serverCapabilities;
+      return this;
+    }
+
+    public Builder toolCall(Tool tool, McpToolCallHandler handler) {
+      tools.put(tool.name(), new RegisteredTool(tool, handler));
+      return this;
+    }
+
+    public McpServer build() {
+      McpRequestHandler handler = new McpRequestHandler(objectMapper, serverName,
+          serverVersion, capabilities, tools);
+      McpHttpServlet httpServlet = new McpHttpServlet(objectMapper, handler);
+      return new McpServer(handler, httpServlet);
+    }
+  }
+
+  static final class RegisteredTool {
+    private final Tool tool;
+    private final McpToolCallHandler handler;
+
+    private RegisteredTool(Tool tool, McpToolCallHandler handler) {
+      this.tool = tool;
+      this.handler = handler;
+    }
+
+    Tool tool() {
+      return tool;
+    }
+
+    CallToolResult call(McpCallContext context, Map<String, Object> arguments) {
+      return handler.call(context, arguments);
+    }
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolCallHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolCallHandler.java
new file mode 100644
index 00000000000..47fed732c7e
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolCallHandler.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.mcp.McpSchema.CallToolResult;
+
+/**
+ * Handler invoked when an MCP client calls a registered tool.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+@FunctionalInterface
+public interface McpToolCallHandler {
+
+  CallToolResult call(McpCallContext context, Map<String, Object> arguments);
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolSchema.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolSchema.java
new file mode 100644
index 00000000000..5065589f17e
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/McpToolSchema.java
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+
+/**
+ * Builds JSON Schema maps for MCP tool input definitions.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+public final class McpToolSchema {
+
+  private final Map<String, Object> schema;
+
+  private McpToolSchema() {
+    Map<String, Object> properties = new LinkedHashMap<>();
+    schema = new LinkedHashMap<>();
+    schema.put("type", "object");
+    schema.put("properties", properties);
+  }
+
+  public static McpToolSchema object() {
+    return new McpToolSchema();
+  }
+
+  public static Map<String, Object> emptyObject() {
+    return object().build();
+  }
+
+  public McpToolSchema string(String name, String description) {
+    properties().put(name, typedProperty("string", description));
+    return this;
+  }
+
+  public McpToolSchema stringArray(String name, String description) {
+    Map<String, Object> items = new LinkedHashMap<>();
+    items.put("type", "string");
+    Map<String, Object> property = new LinkedHashMap<>();
+    property.put("type", "array");
+    property.put("items", items);
+    if (description != null) {
+      property.put("description", description);
+    }
+    properties().put(name, property);
+    return this;
+  }
+
+  public Map<String, Object> build() {
+    return schema;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map<String, Object> properties() {
+    return (Map<String, Object>) schema.get("properties");
+  }
+
+  private static Map<String, Object> typedProperty(String type, String description) {
+    Map<String, Object> property = new LinkedHashMap<>();
+    property.put("type", type);
+    if (description != null) {
+      property.put("description", description);
+    }
+    return property;
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/package-info.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/package-info.java
new file mode 100644
index 00000000000..eca22d61138
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/mcp/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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.
+ */
+
+/**
+ * Hadoop-native Model Context Protocol (MCP) HTTP server framework.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+package org.apache.hadoop.mcp;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServer.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServer.java
new file mode 100644
index 00000000000..1412ef19c12
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServer.java
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+import javax.net.ssl.HttpsURLConnection;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileUtil;
+import org.apache.hadoop.security.ssl.KeyStoreTestUtil;
+import org.apache.hadoop.security.ssl.SSLFactory;
+import org.apache.hadoop.test.GenericTestUtils;
+import org.apache.hadoop.util.Shell;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestMcpHttpServer {
+
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+  private static final JacksonMcpJsonMapper JSON_MAPPER = new JacksonMcpJsonMapper();
+  private static final String BASEDIR =
+      GenericTestUtils.getTempPath(TestMcpHttpServer.class.getSimpleName());
+
+  private static Configuration conf;
+  private static String keystoreDir;
+  private static String sslConfDir;
+  private static SSLFactory clientSslFactory;
+
+  @BeforeAll
+  public static void setupSsl() throws Exception {
+    conf = new Configuration();
+    File base = new File(BASEDIR);
+    FileUtil.fullyDelete(base);
+    base.mkdirs();
+    keystoreDir = base.getAbsolutePath();
+    sslConfDir = KeyStoreTestUtil.getClasspathDir(TestMcpHttpServer.class);
+    KeyStoreTestUtil.setupSSLConfig(keystoreDir, sslConfDir, conf, false);
+    Configuration sslConf = KeyStoreTestUtil.getSslConfig();
+    clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, sslConf);
+    clientSslFactory.init();
+    String protocols = Shell.isJavaVersionAtLeast(11)
+        ? "TLSv1.3,TLSv1.2" : "TLSv1.2";
+    conf.set(SSLFactory.SSL_ENABLED_PROTOCOLS_KEY, protocols);
+    sslConf.set(SSLFactory.SSL_ENABLED_PROTOCOLS_KEY, protocols);
+  }
+
+  @AfterAll
+  public static void cleanupSsl() throws Exception {
+    if (clientSslFactory != null) {
+      clientSslFactory.destroy();
+      clientSslFactory = null;
+    }
+    FileUtil.fullyDelete(new File(BASEDIR));
+    KeyStoreTestUtil.cleanupSSLConfig(keystoreDir, sslConfDir);
+  }
+
+  @Test
+  public void testPlainHttpServerRespondsToInitialize() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .build();
+
+    try (McpHttpServer httpServer = McpHttpServer.start(server, new Configuration(),
+        new InetSocketAddress("localhost", 0), "/mcp", false)) {
+      URL url = new URL("http://localhost:" + httpServer.getPort() + "/mcp");
+      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+      conn.setRequestMethod("POST");
+      conn.setDoOutput(true);
+      conn.setRequestProperty("Content-Type", "application/json");
+      byte[] body = ("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
+          + "\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},"
+          + "\"clientInfo\":{\"name\":\"t\",\"version\":\"1\"}}}").getBytes(
+          StandardCharsets.UTF_8);
+      try (OutputStream out = conn.getOutputStream()) {
+        out.write(body);
+      }
+
+      assertEquals(200, conn.getResponseCode());
+      JsonNode response;
+      try (InputStream in = conn.getInputStream()) {
+        response = OBJECT_MAPPER.readTree(in);
+      }
+      assertEquals("2.0", response.get("jsonrpc").asText());
+      assertEquals(McpJsonRpc.PROTOCOL_VERSION,
+          response.get("result").get("protocolVersion").asText());
+      assertEquals("test-server",
+          response.get("result").get("serverInfo").get("name").asText());
+      assertNotNull(conn.getHeaderField(McpRequestHandler.SESSION_HEADER));
+    }
+  }
+
+  @Test
+  public void testStandaloneServerRespondsToInitialize() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .build();
+
+    try (McpHttpServer httpServer = McpHttpServer.start(server, conf,
+        new InetSocketAddress("localhost", 0), "/mcp")) {
+      assertNotNull(httpServer.getConnectorAddress());
+
+      URL url = new URL("https://localhost:" + httpServer.getPort() + "/mcp");
+      HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
+      conn.setSSLSocketFactory(clientSslFactory.createSSLSocketFactory());
+      conn.setRequestMethod("POST");
+      conn.setDoOutput(true);
+      conn.setRequestProperty("Content-Type", "application/json");
+      byte[] body = ("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
+          + "\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},"
+          + "\"clientInfo\":{\"name\":\"t\",\"version\":\"1\"}}}").getBytes(
+          StandardCharsets.UTF_8);
+      try (OutputStream out = conn.getOutputStream()) {
+        out.write(body);
+      }
+
+      assertEquals(200, conn.getResponseCode());
+      JsonNode response;
+      try (InputStream in = conn.getInputStream()) {
+        response = OBJECT_MAPPER.readTree(in);
+      }
+      assertEquals("2.0", response.get("jsonrpc").asText());
+      assertEquals(McpJsonRpc.PROTOCOL_VERSION,
+          response.get("result").get("protocolVersion").asText());
+      assertEquals("test-server",
+          response.get("result").get("serverInfo").get("name").asText());
+      assertNotNull(conn.getHeaderField(McpRequestHandler.SESSION_HEADER));
+    }
+  }
+
+  @Test
+  public void testEmptyBodyReturnsParseError() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    try (McpHttpServer httpServer = McpHttpServer.start(server, new Configuration(),
+        new InetSocketAddress("localhost", 0), "/mcp", false)) {
+      URL url = new URL("http://localhost:" + httpServer.getPort() + "/mcp");
+      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+      conn.setRequestMethod("POST");
+      conn.setDoOutput(true);
+      conn.setRequestProperty("Content-Type", "application/json");
+      conn.getOutputStream().close();
+
+      assertEquals(200, conn.getResponseCode());
+      JsonNode response;
+      try (InputStream in = conn.getInputStream()) {
+        response = OBJECT_MAPPER.readTree(in);
+      }
+      assertEquals(McpJsonRpc.PARSE_ERROR, response.get("error").get("code").asInt());
+      assertTrue(response.has("id"));
+      assertTrue(response.get("id").isNull());
+    }
+  }
+
+  @Test
+  public void testMalformedJsonReturnsParseError() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    try (McpHttpServer httpServer = McpHttpServer.start(server, new Configuration(),
+        new InetSocketAddress("localhost", 0), "/mcp", false)) {
+      URL url = new URL("http://localhost:" + httpServer.getPort() + "/mcp");
+      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+      conn.setRequestMethod("POST");
+      conn.setDoOutput(true);
+      conn.setRequestProperty("Content-Type", "application/json");
+      byte[] body = "{not-json".getBytes(StandardCharsets.UTF_8);
+      try (OutputStream out = conn.getOutputStream()) {
+        out.write(body);
+      }
+
+      assertEquals(200, conn.getResponseCode());
+      JsonNode response;
+      try (InputStream in = conn.getInputStream()) {
+        response = OBJECT_MAPPER.readTree(in);
+      }
+      assertEquals(McpJsonRpc.PARSE_ERROR, response.get("error").get("code").asInt());
+      assertTrue(response.has("id"));
+      assertTrue(response.get("id").isNull());
+    }
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServlet.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServlet.java
new file mode 100644
index 00000000000..a6617b8dbe4
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpHttpServlet.java
@@ -0,0 +1,303 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestMcpHttpServlet {
+
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+  private static final JacksonMcpJsonMapper JSON_MAPPER =
+      new JacksonMcpJsonMapper(OBJECT_MAPPER);
+
+  @Test
+  public void testToolsList() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .toolCall(McpSchema.Tool.of("echo", "Echo input", JSON_MAPPER,
+                "{\"type\":\"object\",\"properties\":{}}"),
+            (context, args) -> McpSchema.CallToolResult.text("ok"))
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(200, response.status());
+    JsonNode body = response.body();
+    assertEquals("2.0", body.get("jsonrpc").asText());
+    assertEquals(1, body.get("id").asInt());
+    assertTrue(body.get("result").get("tools").isArray());
+    assertEquals("echo", body.get("result").get("tools").get(0).get("name").asText());
+  }
+
+  @Test
+  public void testToolsCall() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .toolCall(McpSchema.Tool.of("echo", "Echo input", JSON_MAPPER,
+                "{\"type\":\"object\",\"properties\":{}}"),
+            (context, args) -> McpSchema.CallToolResult.text("{\"value\":\"test\"}"))
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"echo\",\"arguments\":{}}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertEquals("test", OBJECT_MAPPER.readTree(
+        body.get("result").get("content").get(0).get("text").asText()).get("value").asText());
+  }
+
+  @Test
+  public void testUnknownToolReturnsError() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"missing\",\"arguments\":{}}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertNotNull(body.get("error"));
+    assertEquals(McpJsonRpc.INVALID_PARAMS, body.get("error").get("code").asInt());
+  }
+
+  @Test
+  public void testToolHandlerExceptionReturnsErrorResult() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .toolCall(McpSchema.Tool.of("boom", "Throws", JSON_MAPPER,
+                "{\"type\":\"object\",\"properties\":{}}"),
+            (context, args) -> {
+              throw new RuntimeException("boom");
+            })
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"boom\",\"arguments\":{}}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(200, response.status());
+    JsonNode body = response.body();
+    assertEquals(8, body.get("id").asInt());
+    assertTrue(body.has("result"));
+    assertTrue(!body.has("error"));
+    assertTrue(body.get("result").get("isError").asBoolean());
+    assertTrue(body.get("result").get("content").get(0).get("text").asText().contains("boom"));
+  }
+
+  @Test
+  public void testToolHandlerExceptionWithoutMessageUsesFallback() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .toolCall(McpSchema.Tool.of("fail", "Throws without message", JSON_MAPPER,
+                "{\"type\":\"object\",\"properties\":{}}"),
+            (context, args) -> {
+              throw new RuntimeException();
+            })
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"fail\",\"arguments\":{}}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertTrue(body.get("result").get("isError").asBoolean());
+    assertEquals("Tool execution failed",
+        body.get("result").get("content").get(0).get("text").asText());
+  }
+
+  @Test
+  public void testNonObjectArgumentsReturnsInvalidParams() throws Exception {
+    McpServer server = buildEchoServer();
+
+    JsonNode stringArgsRequest = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"echo\",\"arguments\":\"oops\"}}");
+    McpHttpResponse stringArgsResponse = server.getRequestHandler().handle(stringArgsRequest);
+    JsonNode stringArgsBody = stringArgsResponse.body();
+    assertEquals(McpJsonRpc.INVALID_PARAMS, stringArgsBody.get("error").get("code").asInt());
+    assertEquals("arguments must be a JSON object",
+        stringArgsBody.get("error").get("message").asText());
+    assertEquals(5, stringArgsBody.get("id").asInt());
+
+    JsonNode arrayArgsRequest = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"echo\",\"arguments\":[]}}");
+    McpHttpResponse arrayArgsResponse = server.getRequestHandler().handle(arrayArgsRequest);
+    JsonNode arrayArgsBody = arrayArgsResponse.body();
+    assertEquals(McpJsonRpc.INVALID_PARAMS, arrayArgsBody.get("error").get("code").asInt());
+    assertEquals("arguments must be a JSON object",
+        arrayArgsBody.get("error").get("message").asText());
+    assertEquals(6, arrayArgsBody.get("id").asInt());
+  }
+
+  @Test
+  public void testNullArgumentsReturnsInvalidParams() throws Exception {
+    McpServer server = buildEchoServer();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"echo\",\"arguments\":null}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+    JsonNode body = response.body();
+    assertEquals(McpJsonRpc.INVALID_PARAMS, body.get("error").get("code").asInt());
+    assertEquals("arguments must be a JSON object",
+        body.get("error").get("message").asText());
+    assertEquals(8, body.get("id").asInt());
+  }
+
+  @Test
+  public void testOmittedArgumentsUsesEmptyMap() throws Exception {
+    McpServer server = buildEchoServer();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\","
+            + "\"params\":{\"name\":\"echo\"}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(200, response.status());
+    JsonNode body = response.body();
+    assertEquals(7, body.get("id").asInt());
+    assertTrue(body.has("result"));
+  }
+
+  private static McpServer buildEchoServer() {
+    return McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .toolCall(McpSchema.Tool.of("echo", "Echo input", JSON_MAPPER,
+                "{\"type\":\"object\",\"properties\":{}}"),
+            (context, args) -> McpSchema.CallToolResult.text("ok"))
+        .build();
+  }
+
+  @Test
+  public void testNonObjectParamsReturnsInvalidParams() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/list\",\"params\":[]}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(200, response.status());
+    JsonNode body = response.body();
+    assertEquals(McpJsonRpc.INVALID_PARAMS, body.get("error").get("code").asInt());
+    assertEquals("params must be a JSON object", body.get("error").get("message").asText());
+    assertEquals(4, body.get("id").asInt());
+  }
+
+  @Test
+  public void testInvalidEnvelopeRejectedByHandler() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree("{\"id\":1,\"method\":\"tools/list\"}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertEquals(McpJsonRpc.INVALID_REQUEST, body.get("error").get("code").asInt());
+    assertEquals(McpJsonRpc.INVALID_REQUEST_MESSAGE,
+        body.get("error").get("message").asText());
+    assertEquals(1, body.get("id").asInt());
+  }
+
+  @Test
+  public void testInitializeReturns20250618ProtocolVersion() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
+            + "\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},"
+            + "\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0\"}}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertEquals(McpJsonRpc.PROTOCOL_VERSION,
+        body.get("result").get("protocolVersion").asText());
+  }
+
+  @Test
+  public void testStringIdIsAccepted() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":\"req-1\",\"method\":\"tools/list\",\"params\":{}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    JsonNode body = response.body();
+    assertEquals("req-1", body.get("id").asText());
+    assertTrue(body.has("result"));
+  }
+
+  @Test
+  public void testRequestMethodWithoutIdReturnsAccepted() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .capabilities(McpSchema.ServerCapabilities.withTools())
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"params\":{}}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(202, response.status());
+    assertTrue(response.body() == null);
+  }
+
+  @Test
+  public void testNotificationWithoutIdReturnsAccepted() throws Exception {
+    McpServer server = McpServer.sync(JSON_MAPPER)
+        .serverInfo("test-server", "1.0")
+        .build();
+
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}");
+    McpHttpResponse response = server.getRequestHandler().handle(request);
+
+    assertEquals(202, response.status());
+    assertTrue(response.body() == null);
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpJsonRpcValidator.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpJsonRpcValidator.java
new file mode 100644
index 00000000000..4dd495ee5be
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpJsonRpcValidator.java
@@ -0,0 +1,176 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestMcpJsonRpcValidator {
+
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  private McpJsonRpcResponses responses;
+
+  @BeforeEach
+  public void setUp() {
+    responses = new McpJsonRpcResponses(OBJECT_MAPPER);
+  }
+
+  @Test
+  public void testNullRequestIsInvalid() {
+    assertInvalid(null, null);
+  }
+
+  @Test
+  public void testNonObjectRequestIsInvalid() throws Exception {
+    assertInvalid(OBJECT_MAPPER.readTree("[]"), null);
+    assertInvalid(OBJECT_MAPPER.readTree("null"), null);
+  }
+
+  @Test
+  public void testMissingJsonRpcIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree("{\"id\":1,\"method\":\"tools/list\"}");
+    assertInvalid(request, 1);
+  }
+
+  @Test
+  public void testWrongJsonRpcVersionIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"1.0\",\"id\":2,\"method\":\"tools/list\"}");
+    assertInvalid(request, 2);
+  }
+
+  @Test
+  public void testMissingMethodIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree("{\"jsonrpc\":\"2.0\",\"id\":3}");
+    assertInvalid(request, 3);
+  }
+
+  @Test
+  public void testNullParamsIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/list\",\"params\":null}");
+    assertInvalid(request, 3);
+  }
+
+  @Test
+  public void testNullParamsOnNotificationIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\",\"params\":null}");
+    assertInvalid(request, null);
+  }
+
+  @Test
+  public void testValidRequestAccepted() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/list\"}");
+    assertNull(McpJsonRpcValidator.validate(request, responses));
+  }
+
+  @Test
+  public void testStringRequestIdAccepted() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":\"req-1\",\"method\":\"initialize\"}");
+    assertNull(McpJsonRpcValidator.validate(request, responses));
+  }
+
+  @Test
+  public void testMissingRequestIdAcceptedAsNotification() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\"}");
+    assertNull(McpJsonRpcValidator.validate(request, responses));
+  }
+
+  @Test
+  public void testNullRequestIdIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"tools/list\"}");
+    assertInvalid(request, null);
+  }
+
+  @Test
+  public void testObjectRequestIdIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":{\"bad\":true},\"method\":\"tools/list\"}");
+    assertInvalid(request, null);
+  }
+
+  @Test
+  public void testNotificationWithoutIdAccepted() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}");
+    assertNull(McpJsonRpcValidator.validate(request, responses));
+  }
+
+  @Test
+  public void testNotificationWithIdAcceptedAsRequest() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"notifications/initialized\"}");
+    assertNull(McpJsonRpcValidator.validate(request, responses));
+  }
+
+  @Test
+  public void testNotificationWithNullIdIsInvalid() throws Exception {
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"notifications/initialized\"}");
+    assertInvalid(request, null);
+  }
+
+  @Test
+  public void testErrorResponseIncludesNullIdWhenRequestIdUnknown() {
+    McpHttpResponse response = responses.error(null, McpJsonRpc.PARSE_ERROR,
+        McpJsonRpc.PARSE_ERROR_MESSAGE);
+    JsonNode body = response.body();
+    assertTrue(body.has("id"));
+    assertTrue(body.get("id").isNull());
+    assertEquals(McpJsonRpc.PARSE_ERROR, body.get("error").get("code").asInt());
+  }
+
+  @Test
+  public void testIsJsonRpcNotification() throws Exception {
+    JsonNode notification = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}");
+    JsonNode request = OBJECT_MAPPER.readTree(
+        "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}");
+    assertTrue(McpJsonRpcValidator.isJsonRpcNotification(notification));
+    assertFalse(McpJsonRpcValidator.isJsonRpcNotification(request));
+  }
+
+  private void assertInvalid(JsonNode request, Integer expectedId) {
+    McpHttpResponse response = McpJsonRpcValidator.validate(request, responses);
+    assertNotNull(response, "response should not be null");
+    JsonNode body = response.body();
+    assertEquals(McpJsonRpc.INVALID_REQUEST, body.get("error").get("code").asInt());
+    if (expectedId == null) {
+      assertTrue(body.has("id"));
+      assertTrue(body.get("id").isNull());
+    } else {
+      assertEquals(expectedId.intValue(), body.get("id").asInt());
+    }
+  }
+}
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpToolSchema.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpToolSchema.java
new file mode 100644
index 00000000000..bcb994a1af1
--- /dev/null
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/mcp/TestMcpToolSchema.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.hadoop.mcp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+public class TestMcpToolSchema {
+
+  @Test
+  public void testEmptyObject() {
+    Map<String, Object> schema = McpToolSchema.emptyObject();
+    assertEquals("object", schema.get("type"));
+    @SuppressWarnings("unchecked")
+    Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
+    assertTrue(properties.isEmpty());
+  }
+
+  @Test
+  public void testStringAndStringArrayProperties() {
+    Map<String, Object> schema = McpToolSchema.object()
+        .string("user", "Submitting user filter")
+        .stringArray("states", "Application states to include")
+        .build();
+
+    @SuppressWarnings("unchecked")
+    Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
+
+    @SuppressWarnings("unchecked")
+    Map<String, Object> user = (Map<String, Object>) properties.get("user");
+    assertEquals("string", user.get("type"));
+    assertEquals("Submitting user filter", user.get("description"));
+
+    @SuppressWarnings("unchecked")
+    Map<String, Object> states = (Map<String, Object>) properties.get("states");
+    assertEquals("array", states.get("type"));
+    assertEquals("Application states to include", states.get("description"));
+    @SuppressWarnings("unchecked")
+    Map<String, Object> items = (Map<String, Object>) states.get("items");
+    assertEquals("string", items.get("type"));
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: common-commits-unsubscribe@hadoop.apache.org
For additional commands, e-mail: common-commits-help@hadoop.apache.org