-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Add configuration for MCP JSON Rest bridge filter #43400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c4126dd
4584c52
10f3c14
a42a131
2937cd0
0140927
0d48dc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
botengyao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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; | ||
paulhong01 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't prefer
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Get it. Thanks for the explanation. I personally think the API is fine. But our API style may inclined to no But if no Cc @adisuissa WDYT?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| 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", | ||
| ], | ||
| ) |
| 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 |
| 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 |
| 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", | ||
| ], | ||
| ) |
| 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 |
Uh oh!
There was an error while loading. Please reload this page.