diff --git a/CODEOWNERS b/CODEOWNERS index 2cafdc6708828..b0daf76cff291 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -241,6 +241,8 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/filters/http/api_key_auth @wbpcode @sanposhiho # HTTP MCP filter /*/extensions/filters/http/mcp @botengyao @yanavlasov +# HTTP MCP Jon Rest Bridge filter +/*/extensions/filters/http/mcp_json_rest_bridge @paulhong01 @leon-gg @botengyao @yanavlasov # MCP router filter /*/extensions/filters/http/mcp_router @botengyao @yanavlasov @wdauchy @agrawroh # Original IP detection diff --git a/api/BUILD b/api/BUILD index 5e17442f6c402..cf0feb429ae47 100644 --- a/api/BUILD +++ b/api/BUILD @@ -224,6 +224,7 @@ proto_library( "//envoy/extensions/filters/http/local_ratelimit/v3:pkg", "//envoy/extensions/filters/http/lua/v3:pkg", "//envoy/extensions/filters/http/mcp/v3:pkg", + "//envoy/extensions/filters/http/mcp_json_rest_bridge/v3:pkg", "//envoy/extensions/filters/http/mcp_router/v3:pkg", "//envoy/extensions/filters/http/oauth2/v3:pkg", "//envoy/extensions/filters/http/on_demand/v3:pkg", diff --git a/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/BUILD b/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/BUILD new file mode 100644 index 0000000000000..8ee554d4d4f25 --- /dev/null +++ b/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "@xds//udpa/annotations:pkg", + "@xds//xds/annotations/v3:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.proto b/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.proto new file mode 100644 index 0000000000000..091ccd883ac6b --- /dev/null +++ b/api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.proto @@ -0,0 +1,189 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.mcp_json_rest_bridge.v3; + +import "xds/annotations/v3/status.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.mcp_json_rest_bridge.v3"; +option java_outer_classname = "McpJsonRestBridgeProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/mcp_json_rest_bridge/v3;mcp_json_rest_bridgev3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; +option (xds.annotations.v3.file_status).work_in_progress = true; + +// [#protodoc-title: MCP JSON REST Bridge] +// [#extension: envoy.filters.http.mcp_json_rest_bridge] + +// Configuration for the MCP MCP JSON REST Bridge. +// +// This extension translates Model Context Protocol (MCP) JSON-RPC requests into standard JSON-REST +// HTTP requests. This enables existing REST backends to function as MCP servers without native MCP +// support. +// +// Main functionalities: +// +// 1. Transcoding: Converts JSON-RPC request payload to HTTP REST request, and maps JSON response +// back to JSON-RPC. +// 2. Session negotiation: Handles MCP connection prerequisites. +// +// The core logic transforms "tools/call" request into HTTP request following the ``HttpRule`` +// specification. +// +// Example 1: GET request with path and query parameters +// +// .. code-block:: text +// +// tools: { +// name: "getResource" +// http_rule: { +// get: "/v1/projects/{project_id}/resources/{resource_id}" +// // body is omitted for GET +// } +// } +// If tools/call params are: +// { "name": "getResource", "arguments": {"project_id": "foo", "resource_id": "res-789", "view": "FULL"} } +// Translation: +// - Method: GET +// - URL: /v1/projects/foo/resources/res-789?view=FULL +// (Arguments not matching path templates become query parameters.) +// +// Example 2: POST request with wildcard body +// +// .. code-block:: text +// +// tools: { +// name: "createResource" +// http_rule: { +// post: "/v1/projects/{project_id}/resources" +// body: "*" +// } +// } +// If tools/call params are: +// { "name": "createResource", "arguments": {"project_id": "foo", "resource_id": "res-456", "payload": { "data": "some value" }} } +// Translation: +// - Method: POST +// - URL: /v1/projects/foo/resources +// - Body: {"resource_id": "res-456", "payload": { "data": "some value" }} +// (Arguments not used in the path form the body, as per body: "*".) +// +// Example 3: PUT request with a specific field as body +// +// .. code-block:: text +// +// tools: { +// name: "updateResource" +// http_rule: { +// put: "/v1/projects/{project_id}" +// body: "payload" +// } +// } +// If tools/call params are: +// { "name": "updateResource", "arguments": {"project_id": "foo", "resource_id": "res-456", "payload": { "data": "updated value" }} } +// Translation: +// - Method: PUT +// - URL: /v1/projects/foo?resource_id=res-456 +// - Body: {"data": "updated value"} +// (Only the "payload" field from arguments is used as the body. Other arguments not in the +// path, like 'resource_id', become query parameters.) +message McpJsonRestBridge { + // General server information. + ServerInfo server_info = 1; + + // Configuration for the MCP tools. + ServerToolConfig tool_config = 2; +} + +// Configuration for the server metadata. +message ServerInfo { + // Lists the MCP protocol versions supported by this MCP endpoint. + // + // - If provided: The extension enforces version negotiation according to the MCP specification: + // https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation + // - If not provided: The extension accepts any version sent by the client during negotiation and + // skips validation of the mcp-protocol-version header on subsequent requests. + // + // Example values: ["2025-11-25", "2025-06-18"] + repeated string supported_protocol_version = 1; + + // Optional description of the server. + string description = 2; +} + +// Configuration for the MCP tool capability of the server. +message ServerToolConfig { + // List of MCP tools configurations. + repeated ToolConfig tools = 1; + + // Whether this server supports notifications for changes to the tool list. + bool list_changed = 2; + + // Optional configuration to transcode the tools/list requests to a standard HTTP request. + // + // - If provided: The extension transcodes the request and forwards it down the filter chain. + // The response (whether from an upstream backend, a configured ``direct_response``, or another + // extension) MUST be a JSON body strictly matching the MCP ``ListToolsResult`` schema. + // Ref: https://modelcontextprotocol.io/specification/2025-11-25/schema#listtoolsresult + // - If not provided: The ``tools/list`` request is passed through. This allows subsequent + // extension or the backend itself to handle the tools/list request if they support it. + HttpRule tool_list_url_remap = 3; +} + +// Configuration for a specific MCP tool. +message ToolConfig { + // Name of the tool. + string name = 1 [(validate.rules).string = {min_len: 1}]; + + // The HTTP configuration rules that apply to the normal backend. + HttpRule http_rule = 2; +} + +// Defines the schema of the JSON-RPC to REST mapping. It specifies how the "arguments" +// in a tools/call request are mapped to the URL path, query parameters, and HTTP request body. +// +// Mapping Rules: +// +// 1. Path: Fields defined in the path template (e.g., ``/v1/resources/{id}``) are extracted from +// arguments and placed in the URL. +// 2. Body: Determined by the ``body`` field. +// +// - If "*": All arguments not used in the path become the HTTP JSON body. +// - If specify a field: Only that specific argument becomes the HTTP JSON body. +// - If empty: No body is sent. +// +// 3. Query: Any leaf arguments not mapped to Path or Body are added as URL query parameters. +// [#next-free-field: 7] +message HttpRule { + // Determines the HTTP method and the URL path template. + // + // Path templating uses curly braces ``{}`` to mark a section of the URL path as replaceable. + // Each template variable MUST correspond to a field in the JSON-RPC "arguments". + // Use dot-notation to access fields within nested objects (e.g., "user.id" maps the value of the + // "id" field inside "user"). + oneof pattern { + // Maps to HTTP GET. + string get = 1; + + // Maps to HTTP PUT. + string put = 2; + + // Maps to HTTP POST. + string post = 3; + + // Maps to HTTP DELETE. + string delete = 4; + + // Maps to HTTP PATCH. + string patch = 5; + } + + // The name of the request field whose value is mapped to the HTTP request body. + // + // - If "*": All fields not bound by the path template are mapped to the request body. + // - If specify a field: This specific field is mapped to the body. Uses dot-notation for nested + // fields (e.g., "user.data" maps the value of the "data" field inside "user"). + // - If omitted: There is no HTTP request body; fields not in the path become query parameters. + string body = 6; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 099cafed56439..b75a4e139e952 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -163,6 +163,7 @@ proto_library( "//envoy/extensions/filters/http/local_ratelimit/v3:pkg", "//envoy/extensions/filters/http/lua/v3:pkg", "//envoy/extensions/filters/http/mcp/v3:pkg", + "//envoy/extensions/filters/http/mcp_json_rest_bridge/v3:pkg", "//envoy/extensions/filters/http/mcp_router/v3:pkg", "//envoy/extensions/filters/http/oauth2/v3:pkg", "//envoy/extensions/filters/http/on_demand/v3:pkg", diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 693006c32082f..6098fb0cca4e5 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -192,6 +192,7 @@ EXTENSIONS = { "envoy.filters.http.json_to_metadata": "//source/extensions/filters/http/json_to_metadata:config", "envoy.filters.http.jwt_authn": "//source/extensions/filters/http/jwt_authn:config", "envoy.filters.http.mcp": "//source/extensions/filters/http/mcp:config", + "envoy.filters.http.mcp_json_rest_bridge": "//source/extensions/filters/http/mcp_json_rest_bridge:config", "envoy.filters.http.mcp_router": "//source/extensions/filters/http/mcp_router:config", "envoy.filters.http.rate_limit_quota": "//source/extensions/filters/http/rate_limit_quota:config", # Disabled by default. kill_request is not built into most prebuilt images. diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 8aa44eb5d4c7d..5c7886fd2c9c3 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -607,6 +607,13 @@ envoy.filters.http.mcp: type_urls: - envoy.extensions.filters.http.mcp.v3.Mcp - envoy.extensions.filters.http.mcp.v3.McpOverride +envoy.filters.http.mcp_json_rest_bridge: + categories: + - envoy.filters.http + security_posture: unknown + status: alpha + type_urls: + - envoy.extensions.filters.http.mcp_json_rest_bridge.v3.McpJsonRestBridge envoy.filters.http.mcp_router: categories: - envoy.filters.http diff --git a/source/extensions/filters/http/mcp_json_rest_bridge/BUILD b/source/extensions/filters/http/mcp_json_rest_bridge/BUILD new file mode 100644 index 0000000000000..a90fed95d0320 --- /dev/null +++ b/source/extensions/filters/http/mcp_json_rest_bridge/BUILD @@ -0,0 +1,20 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + "//envoy/registry", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/mcp_json_rest_bridge/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/mcp_json_rest_bridge/config.cc b/source/extensions/filters/http/mcp_json_rest_bridge/config.cc new file mode 100644 index 0000000000000..e5709d37ef90f --- /dev/null +++ b/source/extensions/filters/http/mcp_json_rest_bridge/config.cc @@ -0,0 +1,27 @@ +#include "source/extensions/filters/http/mcp_json_rest_bridge/config.h" + +#include "envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.pb.h" +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace McpJsonRestBridge { + +Http::FilterFactoryCb McpJsonRestBridgeFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::mcp_json_rest_bridge::v3::McpJsonRestBridge&, + const std::string&, Server::Configuration::FactoryContext&) { + + return [](Http::FilterChainFactoryCallbacks&) -> void {}; +} + +/** + * Static registration for the MCP JSON REST bridge filter. @see RegisterFactory. + */ +REGISTER_FACTORY(McpJsonRestBridgeFilterConfigFactory, + Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace McpJsonRestBridge +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/mcp_json_rest_bridge/config.h b/source/extensions/filters/http/mcp_json_rest_bridge/config.h new file mode 100644 index 0000000000000..9e7510134a754 --- /dev/null +++ b/source/extensions/filters/http/mcp_json_rest_bridge/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include "envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.pb.h" +#include "envoy/extensions/filters/http/mcp_json_rest_bridge/v3/mcp_json_rest_bridge.pb.validate.h" + +#include "source/extensions/filters/http/common/factory_base.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace McpJsonRestBridge { + +/** + * Config factory for MCP JSON REST bridge filter. + */ +class McpJsonRestBridgeFilterConfigFactory + : public Common::FactoryBase< + envoy::extensions::filters::http::mcp_json_rest_bridge::v3::McpJsonRestBridge> { +public: + McpJsonRestBridgeFilterConfigFactory() : FactoryBase("envoy.filters.http.mcp_json_rest_bridge") {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::mcp_json_rest_bridge::v3::McpJsonRestBridge& + proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace McpJsonRestBridge +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/mcp_json_rest_bridge/BUILD b/test/extensions/filters/http/mcp_json_rest_bridge/BUILD new file mode 100644 index 0000000000000..39235b3bc06fc --- /dev/null +++ b/test/extensions/filters/http/mcp_json_rest_bridge/BUILD @@ -0,0 +1,19 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//source/extensions/filters/http/mcp_json_rest_bridge:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/filters/http/mcp_json_rest_bridge/config_test.cc b/test/extensions/filters/http/mcp_json_rest_bridge/config_test.cc new file mode 100644 index 0000000000000..456bafc8a6915 --- /dev/null +++ b/test/extensions/filters/http/mcp_json_rest_bridge/config_test.cc @@ -0,0 +1,40 @@ +#include "source/extensions/filters/http/mcp_json_rest_bridge/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace McpJsonRestBridge { +namespace { + +using ::testing::NiceMock; + +TEST(McpJsonRestBridgeFilterConfigFactoryTest, RegisterAndCreateFilterWithEmptyConfig) { + auto* factory = + Registry::FactoryRegistry::getFactory( + "envoy.filters.http.mcp_json_rest_bridge"); + ASSERT_NE(factory, nullptr); + + envoy::extensions::filters::http::mcp_json_rest_bridge::v3::McpJsonRestBridge proto_config; + NiceMock context; + absl::StatusOr cb = + factory->createFilterFactoryFromProto(proto_config, "stats", context); + ASSERT_TRUE(cb.ok()); + + // TODO(paulhong01): Update the following verification once the proto config is processed + // properly. + NiceMock filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamFilter).Times(0); + (*cb)(filter_callbacks); +} + +} // namespace +} // namespace McpJsonRestBridge +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy