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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions api/envoy/extensions/filters/http/mcp_json_rest_bridge/v3/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +165 to +180
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't prefer oneof. And this is more like an enum, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tagged union, not an enum. These fields must carry a payload: the URL Path Template.

The structure get: "/v1/foo" concisely defines both the HTTP Method (via the field ID) and the Path (via the string value) in a single entry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get it. Thanks for the explanation.
Although we still could use an enum + string/path to complete same feature?

I personally think the API is fine. But our API style may inclined to no oneof and check it manually in the source code.

But if no oneof, for such many fields, it's not friendly to check them.

Cc @adisuissa WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. An enum + string path can also work. I personally think enum + string path is little bit verbose. Also, The oneof approach structurally guarantees that if a rule exists, the method is known.

That being said, I'm open to change if the reviewer/style guide has any preference. Let me know what you think. Thanks!


// 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;
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions source/extensions/filters/http/mcp_json_rest_bridge/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
27 changes: 27 additions & 0 deletions source/extensions/filters/http/mcp_json_rest_bridge/config.cc
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions source/extensions/filters/http/mcp_json_rest_bridge/config.h
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions test/extensions/filters/http/mcp_json_rest_bridge/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
40 changes: 40 additions & 0 deletions test/extensions/filters/http/mcp_json_rest_bridge/config_test.cc
Original file line number Diff line number Diff line change
@@ -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<Server::Configuration::NamedHttpFilterConfigFactory>::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<Server::Configuration::MockFactoryContext> context;
absl::StatusOr<Http::FilterFactoryCb> 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<Http::MockFilterChainFactoryCallbacks> filter_callbacks;
EXPECT_CALL(filter_callbacks, addStreamFilter).Times(0);
(*cb)(filter_callbacks);
}

} // namespace
} // namespace McpJsonRestBridge
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy