diff --git a/api/resource.pb.go b/api/resource.pb.go index 9abc40db9..1e29346f6 100644 --- a/api/resource.pb.go +++ b/api/resource.pb.go @@ -6103,13 +6103,14 @@ func (x *MCPBackend) GetFailureMode() MCPBackend_FailureMode { } type MCPTarget struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Backend *BackendReference `protobuf:"bytes,2,opt,name=backend,proto3" json:"backend,omitempty"` - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - Protocol MCPTarget_Protocol `protobuf:"varint,4,opt,name=protocol,proto3,enum=agentgateway.dev.resource.MCPTarget_Protocol" json:"protocol,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Backend *BackendReference `protobuf:"bytes,2,opt,name=backend,proto3" json:"backend,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + Protocol MCPTarget_Protocol `protobuf:"varint,4,opt,name=protocol,proto3,enum=agentgateway.dev.resource.MCPTarget_Protocol" json:"protocol,omitempty"` + ResponseCompression *MCPTarget_ResponseCompression `protobuf:"bytes,5,opt,name=response_compression,json=responseCompression,proto3" json:"response_compression,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MCPTarget) Reset() { @@ -6170,6 +6171,13 @@ func (x *MCPTarget) GetProtocol() MCPTarget_Protocol { return MCPTarget_UNDEFINED } +func (x *MCPTarget) GetResponseCompression() *MCPTarget_ResponseCompression { + if x != nil { + return x.ResponseCompression + } + return nil +} + type BackendReference struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Kind: @@ -11474,6 +11482,58 @@ func (x *AIBackend_ProviderGroup) GetProviders() []*AIBackend_Provider { return nil } +type MCPTarget_ResponseCompression struct { + state protoimpl.MessageState `protogen:"open.v1"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + Format string `protobuf:"bytes,2,opt,name=format,proto3" json:"format,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MCPTarget_ResponseCompression) Reset() { + *x = MCPTarget_ResponseCompression{} + mi := &file_resource_proto_msgTypes[148] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MCPTarget_ResponseCompression) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MCPTarget_ResponseCompression) ProtoMessage() {} + +func (x *MCPTarget_ResponseCompression) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[148] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MCPTarget_ResponseCompression.ProtoReflect.Descriptor instead. +func (*MCPTarget_ResponseCompression) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{53, 0} +} + +func (x *MCPTarget_ResponseCompression) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *MCPTarget_ResponseCompression) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + type BackendReference_Service struct { state protoimpl.MessageState `protogen:"open.v1"` Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -11484,7 +11544,7 @@ type BackendReference_Service struct { func (x *BackendReference_Service) Reset() { *x = BackendReference_Service{} - mi := &file_resource_proto_msgTypes[148] + mi := &file_resource_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11496,7 +11556,7 @@ func (x *BackendReference_Service) String() string { func (*BackendReference_Service) ProtoMessage() {} func (x *BackendReference_Service) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[148] + mi := &file_resource_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12432,12 +12492,16 @@ const file_resource_proto_rawDesc = "" + "\x06ALWAYS\x10\x01\"-\n" + "\vFailureMode\x12\x0f\n" + "\vFAIL_CLOSED\x10\x00\x12\r\n" + - "\tFAIL_OPEN\x10\x01\"\xfe\x01\n" + + "\tFAIL_OPEN\x10\x01\"\xb4\x03\n" + "\tMCPTarget\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12E\n" + "\abackend\x18\x02 \x01(\v2+.agentgateway.dev.resource.BackendReferenceR\abackend\x12\x12\n" + "\x04path\x18\x03 \x01(\tR\x04path\x12I\n" + - "\bprotocol\x18\x04 \x01(\x0e2-.agentgateway.dev.resource.MCPTarget.ProtocolR\bprotocol\"7\n" + + "\bprotocol\x18\x04 \x01(\x0e2-.agentgateway.dev.resource.MCPTarget.ProtocolR\bprotocol\x12k\n" + + "\x14response_compression\x18\x05 \x01(\v28.agentgateway.dev.resource.MCPTarget.ResponseCompressionR\x13responseCompression\x1aG\n" + + "\x13ResponseCompression\x12\x18\n" + + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x16\n" + + "\x06format\x18\x02 \x01(\tR\x06format\"7\n" + "\bProtocol\x12\r\n" + "\tUNDEFINED\x10\x00\x12\a\n" + "\x03SSE\x10\x01\x12\x13\n" + @@ -12473,7 +12537,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 31) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 149) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 150) var file_resource_proto_goTypes = []any{ (Protocol)(0), // 0: agentgateway.dev.resource.Protocol (Bind_Protocol)(0), // 1: agentgateway.dev.resource.Bind.Protocol @@ -12654,13 +12718,14 @@ var file_resource_proto_goTypes = []any{ (*AIBackend_Azure)(nil), // 176: agentgateway.dev.resource.AIBackend.Azure (*AIBackend_Provider)(nil), // 177: agentgateway.dev.resource.AIBackend.Provider (*AIBackend_ProviderGroup)(nil), // 178: agentgateway.dev.resource.AIBackend.ProviderGroup - (*BackendReference_Service)(nil), // 179: agentgateway.dev.resource.BackendReference.Service - (*workloadapi.Workload)(nil), // 180: istio.workload.Workload - (*workloadapi.Service)(nil), // 181: istio.workload.Service - (*workloadapi.NamespacedHostname)(nil), // 182: istio.workload.NamespacedHostname - (*durationpb.Duration)(nil), // 183: google.protobuf.Duration - (*structpb.Struct)(nil), // 184: google.protobuf.Struct - (*structpb.Value)(nil), // 185: google.protobuf.Value + (*MCPTarget_ResponseCompression)(nil), // 179: agentgateway.dev.resource.MCPTarget.ResponseCompression + (*BackendReference_Service)(nil), // 180: agentgateway.dev.resource.BackendReference.Service + (*workloadapi.Workload)(nil), // 181: istio.workload.Workload + (*workloadapi.Service)(nil), // 182: istio.workload.Service + (*workloadapi.NamespacedHostname)(nil), // 183: istio.workload.NamespacedHostname + (*durationpb.Duration)(nil), // 184: google.protobuf.Duration + (*structpb.Struct)(nil), // 185: google.protobuf.Struct + (*structpb.Value)(nil), // 186: google.protobuf.Value } var file_resource_proto_depIdxs = []int32{ 32, // 0: agentgateway.dev.resource.Resource.bind:type_name -> agentgateway.dev.resource.Bind @@ -12669,20 +12734,20 @@ var file_resource_proto_depIdxs = []int32{ 41, // 3: agentgateway.dev.resource.Resource.backend:type_name -> agentgateway.dev.resource.Backend 40, // 4: agentgateway.dev.resource.Resource.policy:type_name -> agentgateway.dev.resource.Policy 39, // 5: agentgateway.dev.resource.Resource.tcp_route:type_name -> agentgateway.dev.resource.TCPRoute - 180, // 6: agentgateway.dev.resource.Resource.workload:type_name -> istio.workload.Workload - 181, // 7: agentgateway.dev.resource.Resource.service:type_name -> istio.workload.Service + 181, // 6: agentgateway.dev.resource.Resource.workload:type_name -> istio.workload.Workload + 182, // 7: agentgateway.dev.resource.Resource.service:type_name -> istio.workload.Service 1, // 8: agentgateway.dev.resource.Bind.protocol:type_name -> agentgateway.dev.resource.Bind.Protocol 2, // 9: agentgateway.dev.resource.Bind.tunnel_protocol:type_name -> agentgateway.dev.resource.Bind.TunnelProtocol 35, // 10: agentgateway.dev.resource.ListenerName.listener_set:type_name -> agentgateway.dev.resource.ResourceName 34, // 11: agentgateway.dev.resource.Listener.name:type_name -> agentgateway.dev.resource.ListenerName 0, // 12: agentgateway.dev.resource.Listener.protocol:type_name -> agentgateway.dev.resource.Protocol 42, // 13: agentgateway.dev.resource.Listener.tls:type_name -> agentgateway.dev.resource.TLSConfig - 182, // 14: agentgateway.dev.resource.Route.service_key:type_name -> istio.workload.NamespacedHostname + 183, // 14: agentgateway.dev.resource.Route.service_key:type_name -> istio.workload.NamespacedHostname 33, // 15: agentgateway.dev.resource.Route.name:type_name -> agentgateway.dev.resource.RouteName 59, // 16: agentgateway.dev.resource.Route.matches:type_name -> agentgateway.dev.resource.RouteMatch 71, // 17: agentgateway.dev.resource.Route.backends:type_name -> agentgateway.dev.resource.RouteBackend 76, // 18: agentgateway.dev.resource.Route.traffic_policies:type_name -> agentgateway.dev.resource.TrafficPolicySpec - 182, // 19: agentgateway.dev.resource.TCPRoute.service_key:type_name -> istio.workload.NamespacedHostname + 183, // 19: agentgateway.dev.resource.TCPRoute.service_key:type_name -> istio.workload.NamespacedHostname 33, // 20: agentgateway.dev.resource.TCPRoute.name:type_name -> agentgateway.dev.resource.RouteName 71, // 21: agentgateway.dev.resource.TCPRoute.backends:type_name -> agentgateway.dev.resource.RouteBackend 36, // 22: agentgateway.dev.resource.Policy.name:type_name -> agentgateway.dev.resource.TypedResourceName @@ -12701,9 +12766,9 @@ var file_resource_proto_depIdxs = []int32{ 3, // 35: agentgateway.dev.resource.TLSConfig.min_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion 3, // 36: agentgateway.dev.resource.TLSConfig.max_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion 4, // 37: agentgateway.dev.resource.TLSConfig.mtls_mode:type_name -> agentgateway.dev.resource.TLSConfig.MTLSMode - 183, // 38: agentgateway.dev.resource.Timeout.request:type_name -> google.protobuf.Duration - 183, // 39: agentgateway.dev.resource.Timeout.backend_request:type_name -> google.protobuf.Duration - 183, // 40: agentgateway.dev.resource.Retry.backoff:type_name -> google.protobuf.Duration + 184, // 38: agentgateway.dev.resource.Timeout.request:type_name -> google.protobuf.Duration + 184, // 39: agentgateway.dev.resource.Timeout.backend_request:type_name -> google.protobuf.Duration + 184, // 40: agentgateway.dev.resource.Retry.backoff:type_name -> google.protobuf.Duration 46, // 41: agentgateway.dev.resource.BackendAuthPolicy.passthrough:type_name -> agentgateway.dev.resource.Passthrough 47, // 42: agentgateway.dev.resource.BackendAuthPolicy.key:type_name -> agentgateway.dev.resource.Key 48, // 43: agentgateway.dev.resource.BackendAuthPolicy.gcp:type_name -> agentgateway.dev.resource.Gcp @@ -12724,7 +12789,7 @@ var file_resource_proto_depIdxs = []int32{ 63, // 58: agentgateway.dev.resource.RouteMatch.headers:type_name -> agentgateway.dev.resource.HeaderMatch 62, // 59: agentgateway.dev.resource.RouteMatch.method:type_name -> agentgateway.dev.resource.MethodMatch 61, // 60: agentgateway.dev.resource.RouteMatch.query_params:type_name -> agentgateway.dev.resource.QueryMatch - 183, // 61: agentgateway.dev.resource.CORS.max_age:type_name -> google.protobuf.Duration + 184, // 61: agentgateway.dev.resource.CORS.max_age:type_name -> google.protobuf.Duration 70, // 62: agentgateway.dev.resource.HeaderModifier.add:type_name -> agentgateway.dev.resource.Header 70, // 63: agentgateway.dev.resource.HeaderModifier.set:type_name -> agentgateway.dev.resource.Header 90, // 64: agentgateway.dev.resource.RequestMirrors.mirrors:type_name -> agentgateway.dev.resource.RequestMirrors.Mirror @@ -12734,8 +12799,8 @@ var file_resource_proto_depIdxs = []int32{ 94, // 68: agentgateway.dev.resource.PolicyTarget.route:type_name -> agentgateway.dev.resource.PolicyTarget.RouteTarget 92, // 69: agentgateway.dev.resource.PolicyTarget.backend:type_name -> agentgateway.dev.resource.PolicyTarget.BackendTarget 91, // 70: agentgateway.dev.resource.PolicyTarget.service:type_name -> agentgateway.dev.resource.PolicyTarget.ServiceTarget - 183, // 71: agentgateway.dev.resource.KeepaliveConfig.time:type_name -> google.protobuf.Duration - 183, // 72: agentgateway.dev.resource.KeepaliveConfig.interval:type_name -> google.protobuf.Duration + 184, // 71: agentgateway.dev.resource.KeepaliveConfig.time:type_name -> google.protobuf.Duration + 184, // 72: agentgateway.dev.resource.KeepaliveConfig.interval:type_name -> google.protobuf.Duration 97, // 73: agentgateway.dev.resource.FrontendPolicySpec.tcp:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TCP 96, // 74: agentgateway.dev.resource.FrontendPolicySpec.tls:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TLS 95, // 75: agentgateway.dev.resource.FrontendPolicySpec.http:type_name -> agentgateway.dev.resource.FrontendPolicySpec.HTTP @@ -12787,135 +12852,136 @@ var file_resource_proto_depIdxs = []int32{ 29, // 121: agentgateway.dev.resource.MCPBackend.failure_mode:type_name -> agentgateway.dev.resource.MCPBackend.FailureMode 85, // 122: agentgateway.dev.resource.MCPTarget.backend:type_name -> agentgateway.dev.resource.BackendReference 30, // 123: agentgateway.dev.resource.MCPTarget.protocol:type_name -> agentgateway.dev.resource.MCPTarget.Protocol - 179, // 124: agentgateway.dev.resource.BackendReference.service:type_name -> agentgateway.dev.resource.BackendReference.Service - 85, // 125: agentgateway.dev.resource.RequestMirrors.Mirror.backend:type_name -> agentgateway.dev.resource.BackendReference - 183, // 126: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http1_idle_timeout:type_name -> google.protobuf.Duration - 183, // 127: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http2_keepalive_interval:type_name -> google.protobuf.Duration - 183, // 128: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http2_keepalive_timeout:type_name -> google.protobuf.Duration - 183, // 129: agentgateway.dev.resource.FrontendPolicySpec.TLS.handshake_timeout:type_name -> google.protobuf.Duration - 86, // 130: agentgateway.dev.resource.FrontendPolicySpec.TLS.alpn:type_name -> agentgateway.dev.resource.Alpn - 5, // 131: agentgateway.dev.resource.FrontendPolicySpec.TLS.cipher_suites:type_name -> agentgateway.dev.resource.TLSConfig.CipherSuite - 3, // 132: agentgateway.dev.resource.FrontendPolicySpec.TLS.min_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion - 3, // 133: agentgateway.dev.resource.FrontendPolicySpec.TLS.max_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion - 73, // 134: agentgateway.dev.resource.FrontendPolicySpec.TCP.keepalives:type_name -> agentgateway.dev.resource.KeepaliveConfig - 103, // 135: agentgateway.dev.resource.FrontendPolicySpec.Logging.fields:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.Fields - 104, // 136: agentgateway.dev.resource.FrontendPolicySpec.Logging.otlp_access_log:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog - 85, // 137: agentgateway.dev.resource.FrontendPolicySpec.Tracing.provider_backend:type_name -> agentgateway.dev.resource.BackendReference - 101, // 138: agentgateway.dev.resource.FrontendPolicySpec.Tracing.attributes:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TracingAttribute - 101, // 139: agentgateway.dev.resource.FrontendPolicySpec.Tracing.resources:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TracingAttribute - 7, // 140: agentgateway.dev.resource.FrontendPolicySpec.Tracing.protocol:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Tracing.Protocol - 102, // 141: agentgateway.dev.resource.FrontendPolicySpec.Logging.Fields.add:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.Field - 85, // 142: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.provider_backend:type_name -> agentgateway.dev.resource.BackendReference - 77, // 143: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 6, // 144: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.protocol:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.Protocol - 119, // 145: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.descriptors:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor - 85, // 146: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.target:type_name -> agentgateway.dev.resource.BackendReference - 10, // 147: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.FailureMode - 183, // 148: agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.fill_interval:type_name -> google.protobuf.Duration - 11, // 149: agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.type:type_name -> agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.Type - 85, // 150: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.target:type_name -> agentgateway.dev.resource.BackendReference - 122, // 151: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.grpc:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol - 123, // 152: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.http:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol - 12, // 153: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.FailureMode - 121, // 154: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.include_request_body:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.BodyOptions - 75, // 155: agentgateway.dev.resource.TrafficPolicySpec.JWTProvider.jwt_validation_options:type_name -> agentgateway.dev.resource.JWTValidationOptions - 13, // 156: agentgateway.dev.resource.TrafficPolicySpec.JWT.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWT.Mode - 109, // 157: agentgateway.dev.resource.TrafficPolicySpec.JWT.providers:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWTProvider - 128, // 158: agentgateway.dev.resource.TrafficPolicySpec.JWT.mcp:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP - 14, // 159: agentgateway.dev.resource.TrafficPolicySpec.BasicAuthentication.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.BasicAuthentication.Mode - 129, // 160: agentgateway.dev.resource.TrafficPolicySpec.APIKey.api_keys:type_name -> agentgateway.dev.resource.TrafficPolicySpec.APIKey.User - 15, // 161: agentgateway.dev.resource.TrafficPolicySpec.APIKey.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.APIKey.Mode - 130, // 162: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.request:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform - 130, // 163: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.response:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform - 85, // 164: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.target:type_name -> agentgateway.dev.resource.BackendReference - 16, // 165: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.FailureMode - 133, // 166: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.request_attributes:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.RequestAttributesEntry - 134, // 167: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.response_attributes:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.ResponseAttributesEntry - 135, // 168: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.metadata_context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.MetadataContextEntry - 17, // 169: agentgateway.dev.resource.TrafficPolicySpec.HostRewrite.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HostRewrite.Mode - 120, // 170: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor.entries:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Entry - 9, // 171: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor.type:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Type - 124, // 172: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.ContextEntry - 125, // 173: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.MetadataEntry - 126, // 174: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.add_request_headers:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.AddRequestHeadersEntry - 127, // 175: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.MetadataEntry - 24, // 176: agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP.provider:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.McpIDP - 167, // 177: agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP.resource_metadata:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata - 184, // 178: agentgateway.dev.resource.TrafficPolicySpec.APIKey.User.metadata:type_name -> google.protobuf.Struct - 114, // 179: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.set:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HeaderTransformation - 114, // 180: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.add:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HeaderTransformation - 115, // 181: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.body:type_name -> agentgateway.dev.resource.TrafficPolicySpec.BodyTransformation - 131, // 182: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.MetadataEntry - 136, // 183: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext.context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext.ContextEntry - 132, // 184: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.MetadataContextEntry.value:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext - 160, // 185: agentgateway.dev.resource.BackendPolicySpec.Ai.prompt_guard:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard - 162, // 186: agentgateway.dev.resource.BackendPolicySpec.Ai.defaults:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.DefaultsEntry - 163, // 187: agentgateway.dev.resource.BackendPolicySpec.Ai.overrides:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.OverridesEntry - 164, // 188: agentgateway.dev.resource.BackendPolicySpec.Ai.transformations:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.TransformationsEntry - 149, // 189: agentgateway.dev.resource.BackendPolicySpec.Ai.prompts:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment - 165, // 190: agentgateway.dev.resource.BackendPolicySpec.Ai.model_aliases:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ModelAliasesEntry - 161, // 191: agentgateway.dev.resource.BackendPolicySpec.Ai.prompt_caching:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptCaching - 166, // 192: agentgateway.dev.resource.BackendPolicySpec.Ai.routes:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RoutesEntry - 85, // 193: agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.endpoint_picker:type_name -> agentgateway.dev.resource.BackendReference - 21, // 194: agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.failure_mode:type_name -> agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.FailureMode - 183, // 195: agentgateway.dev.resource.BackendPolicySpec.Eviction.duration:type_name -> google.protobuf.Duration - 140, // 196: agentgateway.dev.resource.BackendPolicySpec.Health.eviction:type_name -> agentgateway.dev.resource.BackendPolicySpec.Eviction - 22, // 197: agentgateway.dev.resource.BackendPolicySpec.BackendTLS.verification:type_name -> agentgateway.dev.resource.BackendPolicySpec.BackendTLS.VerificationMode - 86, // 198: agentgateway.dev.resource.BackendPolicySpec.BackendTLS.alpn:type_name -> agentgateway.dev.resource.Alpn - 23, // 199: agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.version:type_name -> agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.HttpVersion - 183, // 200: agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.request_timeout:type_name -> google.protobuf.Duration - 85, // 201: agentgateway.dev.resource.BackendPolicySpec.BackendTunnel.proxy:type_name -> agentgateway.dev.resource.BackendReference - 73, // 202: agentgateway.dev.resource.BackendPolicySpec.BackendTCP.keepalive:type_name -> agentgateway.dev.resource.KeepaliveConfig - 183, // 203: agentgateway.dev.resource.BackendPolicySpec.BackendTCP.connect_timeout:type_name -> google.protobuf.Duration - 24, // 204: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.provider:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.McpIDP - 167, // 205: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.resource_metadata:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata - 25, // 206: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.mode:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.Mode - 75, // 207: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.jwt_validation_options:type_name -> agentgateway.dev.resource.JWTValidationOptions - 148, // 208: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment.append:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Message - 148, // 209: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment.prepend:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Message - 18, // 210: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRule.builtin:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BuiltinRegexRule - 19, // 211: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules.action:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ActionKind - 150, // 212: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules.rules:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRule - 85, // 213: agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook.backend:type_name -> agentgateway.dev.resource.BackendReference - 63, // 214: agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook.forward_header_matches:type_name -> agentgateway.dev.resource.HeaderMatch - 77, // 215: agentgateway.dev.resource.BackendPolicySpec.Ai.Moderation.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 77, // 216: agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 77, // 217: agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 77, // 218: agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 157, // 219: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.rejection:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestRejection - 151, // 220: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.regex:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules - 152, // 221: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.webhook:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook - 155, // 222: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.google_model_armor:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor - 154, // 223: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.bedrock_guardrails:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails - 156, // 224: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.azure_content_safety:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety - 157, // 225: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.rejection:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestRejection - 151, // 226: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.regex:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules - 152, // 227: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.webhook:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook - 153, // 228: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.openai_moderation:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Moderation - 155, // 229: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.google_model_armor:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor - 154, // 230: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.bedrock_guardrails:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails - 156, // 231: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.azure_content_safety:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety - 159, // 232: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard.request:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard - 158, // 233: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard.response:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard - 20, // 234: agentgateway.dev.resource.BackendPolicySpec.Ai.RoutesEntry.value:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RouteType - 168, // 235: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.extra:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.ExtraEntry - 185, // 236: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.ExtraEntry.value:type_name -> google.protobuf.Value - 26, // 237: agentgateway.dev.resource.AIBackend.Azure.resource_type:type_name -> agentgateway.dev.resource.AIBackend.AzureResourceType - 169, // 238: agentgateway.dev.resource.AIBackend.Provider.host_override:type_name -> agentgateway.dev.resource.AIBackend.HostOverride - 170, // 239: agentgateway.dev.resource.AIBackend.Provider.openai:type_name -> agentgateway.dev.resource.AIBackend.OpenAI - 171, // 240: agentgateway.dev.resource.AIBackend.Provider.gemini:type_name -> agentgateway.dev.resource.AIBackend.Gemini - 172, // 241: agentgateway.dev.resource.AIBackend.Provider.vertex:type_name -> agentgateway.dev.resource.AIBackend.Vertex - 173, // 242: agentgateway.dev.resource.AIBackend.Provider.anthropic:type_name -> agentgateway.dev.resource.AIBackend.Anthropic - 174, // 243: agentgateway.dev.resource.AIBackend.Provider.bedrock:type_name -> agentgateway.dev.resource.AIBackend.Bedrock - 175, // 244: agentgateway.dev.resource.AIBackend.Provider.azureopenai:type_name -> agentgateway.dev.resource.AIBackend.AzureOpenAI - 176, // 245: agentgateway.dev.resource.AIBackend.Provider.azure:type_name -> agentgateway.dev.resource.AIBackend.Azure - 77, // 246: agentgateway.dev.resource.AIBackend.Provider.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec - 177, // 247: agentgateway.dev.resource.AIBackend.ProviderGroup.providers:type_name -> agentgateway.dev.resource.AIBackend.Provider - 248, // [248:248] is the sub-list for method output_type - 248, // [248:248] is the sub-list for method input_type - 248, // [248:248] is the sub-list for extension type_name - 248, // [248:248] is the sub-list for extension extendee - 0, // [0:248] is the sub-list for field type_name + 179, // 124: agentgateway.dev.resource.MCPTarget.response_compression:type_name -> agentgateway.dev.resource.MCPTarget.ResponseCompression + 180, // 125: agentgateway.dev.resource.BackendReference.service:type_name -> agentgateway.dev.resource.BackendReference.Service + 85, // 126: agentgateway.dev.resource.RequestMirrors.Mirror.backend:type_name -> agentgateway.dev.resource.BackendReference + 184, // 127: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http1_idle_timeout:type_name -> google.protobuf.Duration + 184, // 128: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http2_keepalive_interval:type_name -> google.protobuf.Duration + 184, // 129: agentgateway.dev.resource.FrontendPolicySpec.HTTP.http2_keepalive_timeout:type_name -> google.protobuf.Duration + 184, // 130: agentgateway.dev.resource.FrontendPolicySpec.TLS.handshake_timeout:type_name -> google.protobuf.Duration + 86, // 131: agentgateway.dev.resource.FrontendPolicySpec.TLS.alpn:type_name -> agentgateway.dev.resource.Alpn + 5, // 132: agentgateway.dev.resource.FrontendPolicySpec.TLS.cipher_suites:type_name -> agentgateway.dev.resource.TLSConfig.CipherSuite + 3, // 133: agentgateway.dev.resource.FrontendPolicySpec.TLS.min_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion + 3, // 134: agentgateway.dev.resource.FrontendPolicySpec.TLS.max_version:type_name -> agentgateway.dev.resource.TLSConfig.TLSVersion + 73, // 135: agentgateway.dev.resource.FrontendPolicySpec.TCP.keepalives:type_name -> agentgateway.dev.resource.KeepaliveConfig + 103, // 136: agentgateway.dev.resource.FrontendPolicySpec.Logging.fields:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.Fields + 104, // 137: agentgateway.dev.resource.FrontendPolicySpec.Logging.otlp_access_log:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog + 85, // 138: agentgateway.dev.resource.FrontendPolicySpec.Tracing.provider_backend:type_name -> agentgateway.dev.resource.BackendReference + 101, // 139: agentgateway.dev.resource.FrontendPolicySpec.Tracing.attributes:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TracingAttribute + 101, // 140: agentgateway.dev.resource.FrontendPolicySpec.Tracing.resources:type_name -> agentgateway.dev.resource.FrontendPolicySpec.TracingAttribute + 7, // 141: agentgateway.dev.resource.FrontendPolicySpec.Tracing.protocol:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Tracing.Protocol + 102, // 142: agentgateway.dev.resource.FrontendPolicySpec.Logging.Fields.add:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.Field + 85, // 143: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.provider_backend:type_name -> agentgateway.dev.resource.BackendReference + 77, // 144: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 6, // 145: agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.protocol:type_name -> agentgateway.dev.resource.FrontendPolicySpec.Logging.OtlpAccessLog.Protocol + 119, // 146: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.descriptors:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor + 85, // 147: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.target:type_name -> agentgateway.dev.resource.BackendReference + 10, // 148: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.FailureMode + 184, // 149: agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.fill_interval:type_name -> google.protobuf.Duration + 11, // 150: agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.type:type_name -> agentgateway.dev.resource.TrafficPolicySpec.LocalRateLimit.Type + 85, // 151: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.target:type_name -> agentgateway.dev.resource.BackendReference + 122, // 152: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.grpc:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol + 123, // 153: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.http:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol + 12, // 154: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.FailureMode + 121, // 155: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.include_request_body:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.BodyOptions + 75, // 156: agentgateway.dev.resource.TrafficPolicySpec.JWTProvider.jwt_validation_options:type_name -> agentgateway.dev.resource.JWTValidationOptions + 13, // 157: agentgateway.dev.resource.TrafficPolicySpec.JWT.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWT.Mode + 109, // 158: agentgateway.dev.resource.TrafficPolicySpec.JWT.providers:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWTProvider + 128, // 159: agentgateway.dev.resource.TrafficPolicySpec.JWT.mcp:type_name -> agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP + 14, // 160: agentgateway.dev.resource.TrafficPolicySpec.BasicAuthentication.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.BasicAuthentication.Mode + 129, // 161: agentgateway.dev.resource.TrafficPolicySpec.APIKey.api_keys:type_name -> agentgateway.dev.resource.TrafficPolicySpec.APIKey.User + 15, // 162: agentgateway.dev.resource.TrafficPolicySpec.APIKey.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.APIKey.Mode + 130, // 163: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.request:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform + 130, // 164: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.response:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform + 85, // 165: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.target:type_name -> agentgateway.dev.resource.BackendReference + 16, // 166: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.failure_mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.FailureMode + 133, // 167: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.request_attributes:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.RequestAttributesEntry + 134, // 168: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.response_attributes:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.ResponseAttributesEntry + 135, // 169: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.metadata_context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.MetadataContextEntry + 17, // 170: agentgateway.dev.resource.TrafficPolicySpec.HostRewrite.mode:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HostRewrite.Mode + 120, // 171: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor.entries:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Entry + 9, // 172: agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Descriptor.type:type_name -> agentgateway.dev.resource.TrafficPolicySpec.RemoteRateLimit.Type + 124, // 173: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.ContextEntry + 125, // 174: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.GRPCProtocol.MetadataEntry + 126, // 175: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.add_request_headers:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.AddRequestHeadersEntry + 127, // 176: agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExternalAuth.HTTPProtocol.MetadataEntry + 24, // 177: agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP.provider:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.McpIDP + 167, // 178: agentgateway.dev.resource.TrafficPolicySpec.JWT.MCP.resource_metadata:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata + 185, // 179: agentgateway.dev.resource.TrafficPolicySpec.APIKey.User.metadata:type_name -> google.protobuf.Struct + 114, // 180: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.set:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HeaderTransformation + 114, // 181: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.add:type_name -> agentgateway.dev.resource.TrafficPolicySpec.HeaderTransformation + 115, // 182: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.body:type_name -> agentgateway.dev.resource.TrafficPolicySpec.BodyTransformation + 131, // 183: agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.metadata:type_name -> agentgateway.dev.resource.TrafficPolicySpec.TransformationPolicy.Transform.MetadataEntry + 136, // 184: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext.context:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext.ContextEntry + 132, // 185: agentgateway.dev.resource.TrafficPolicySpec.ExtProc.MetadataContextEntry.value:type_name -> agentgateway.dev.resource.TrafficPolicySpec.ExtProc.NamespacedMetadataContext + 160, // 186: agentgateway.dev.resource.BackendPolicySpec.Ai.prompt_guard:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard + 162, // 187: agentgateway.dev.resource.BackendPolicySpec.Ai.defaults:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.DefaultsEntry + 163, // 188: agentgateway.dev.resource.BackendPolicySpec.Ai.overrides:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.OverridesEntry + 164, // 189: agentgateway.dev.resource.BackendPolicySpec.Ai.transformations:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.TransformationsEntry + 149, // 190: agentgateway.dev.resource.BackendPolicySpec.Ai.prompts:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment + 165, // 191: agentgateway.dev.resource.BackendPolicySpec.Ai.model_aliases:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ModelAliasesEntry + 161, // 192: agentgateway.dev.resource.BackendPolicySpec.Ai.prompt_caching:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.PromptCaching + 166, // 193: agentgateway.dev.resource.BackendPolicySpec.Ai.routes:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RoutesEntry + 85, // 194: agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.endpoint_picker:type_name -> agentgateway.dev.resource.BackendReference + 21, // 195: agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.failure_mode:type_name -> agentgateway.dev.resource.BackendPolicySpec.InferenceRouting.FailureMode + 184, // 196: agentgateway.dev.resource.BackendPolicySpec.Eviction.duration:type_name -> google.protobuf.Duration + 140, // 197: agentgateway.dev.resource.BackendPolicySpec.Health.eviction:type_name -> agentgateway.dev.resource.BackendPolicySpec.Eviction + 22, // 198: agentgateway.dev.resource.BackendPolicySpec.BackendTLS.verification:type_name -> agentgateway.dev.resource.BackendPolicySpec.BackendTLS.VerificationMode + 86, // 199: agentgateway.dev.resource.BackendPolicySpec.BackendTLS.alpn:type_name -> agentgateway.dev.resource.Alpn + 23, // 200: agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.version:type_name -> agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.HttpVersion + 184, // 201: agentgateway.dev.resource.BackendPolicySpec.BackendHTTP.request_timeout:type_name -> google.protobuf.Duration + 85, // 202: agentgateway.dev.resource.BackendPolicySpec.BackendTunnel.proxy:type_name -> agentgateway.dev.resource.BackendReference + 73, // 203: agentgateway.dev.resource.BackendPolicySpec.BackendTCP.keepalive:type_name -> agentgateway.dev.resource.KeepaliveConfig + 184, // 204: agentgateway.dev.resource.BackendPolicySpec.BackendTCP.connect_timeout:type_name -> google.protobuf.Duration + 24, // 205: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.provider:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.McpIDP + 167, // 206: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.resource_metadata:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata + 25, // 207: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.mode:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.Mode + 75, // 208: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.jwt_validation_options:type_name -> agentgateway.dev.resource.JWTValidationOptions + 148, // 209: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment.append:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Message + 148, // 210: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptEnrichment.prepend:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Message + 18, // 211: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRule.builtin:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BuiltinRegexRule + 19, // 212: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules.action:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ActionKind + 150, // 213: agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules.rules:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRule + 85, // 214: agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook.backend:type_name -> agentgateway.dev.resource.BackendReference + 63, // 215: agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook.forward_header_matches:type_name -> agentgateway.dev.resource.HeaderMatch + 77, // 216: agentgateway.dev.resource.BackendPolicySpec.Ai.Moderation.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 77, // 217: agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 77, // 218: agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 77, // 219: agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 157, // 220: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.rejection:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestRejection + 151, // 221: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.regex:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules + 152, // 222: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.webhook:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook + 155, // 223: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.google_model_armor:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor + 154, // 224: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.bedrock_guardrails:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails + 156, // 225: agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard.azure_content_safety:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety + 157, // 226: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.rejection:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestRejection + 151, // 227: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.regex:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RegexRules + 152, // 228: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.webhook:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Webhook + 153, // 229: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.openai_moderation:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.Moderation + 155, // 230: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.google_model_armor:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.GoogleModelArmor + 154, // 231: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.bedrock_guardrails:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.BedrockGuardrails + 156, // 232: agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard.azure_content_safety:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.AzureContentSafety + 159, // 233: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard.request:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RequestGuard + 158, // 234: agentgateway.dev.resource.BackendPolicySpec.Ai.PromptGuard.response:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.ResponseGuard + 20, // 235: agentgateway.dev.resource.BackendPolicySpec.Ai.RoutesEntry.value:type_name -> agentgateway.dev.resource.BackendPolicySpec.Ai.RouteType + 168, // 236: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.extra:type_name -> agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.ExtraEntry + 186, // 237: agentgateway.dev.resource.BackendPolicySpec.McpAuthentication.ResourceMetadata.ExtraEntry.value:type_name -> google.protobuf.Value + 26, // 238: agentgateway.dev.resource.AIBackend.Azure.resource_type:type_name -> agentgateway.dev.resource.AIBackend.AzureResourceType + 169, // 239: agentgateway.dev.resource.AIBackend.Provider.host_override:type_name -> agentgateway.dev.resource.AIBackend.HostOverride + 170, // 240: agentgateway.dev.resource.AIBackend.Provider.openai:type_name -> agentgateway.dev.resource.AIBackend.OpenAI + 171, // 241: agentgateway.dev.resource.AIBackend.Provider.gemini:type_name -> agentgateway.dev.resource.AIBackend.Gemini + 172, // 242: agentgateway.dev.resource.AIBackend.Provider.vertex:type_name -> agentgateway.dev.resource.AIBackend.Vertex + 173, // 243: agentgateway.dev.resource.AIBackend.Provider.anthropic:type_name -> agentgateway.dev.resource.AIBackend.Anthropic + 174, // 244: agentgateway.dev.resource.AIBackend.Provider.bedrock:type_name -> agentgateway.dev.resource.AIBackend.Bedrock + 175, // 245: agentgateway.dev.resource.AIBackend.Provider.azureopenai:type_name -> agentgateway.dev.resource.AIBackend.AzureOpenAI + 176, // 246: agentgateway.dev.resource.AIBackend.Provider.azure:type_name -> agentgateway.dev.resource.AIBackend.Azure + 77, // 247: agentgateway.dev.resource.AIBackend.Provider.inline_policies:type_name -> agentgateway.dev.resource.BackendPolicySpec + 177, // 248: agentgateway.dev.resource.AIBackend.ProviderGroup.providers:type_name -> agentgateway.dev.resource.AIBackend.Provider + 249, // [249:249] is the sub-list for method output_type + 249, // [249:249] is the sub-list for method input_type + 249, // [249:249] is the sub-list for extension type_name + 249, // [249:249] is the sub-list for extension extendee + 0, // [0:249] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -13130,7 +13196,7 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)), NumEnums: 31, - NumMessages: 149, + NumMessages: 150, NumExtensions: 0, NumServices: 0, }, diff --git a/api/resource_json.gen.go b/api/resource_json.gen.go index d93381401..77a26b9e1 100644 --- a/api/resource_json.gen.go +++ b/api/resource_json.gen.go @@ -1447,6 +1447,17 @@ func (this *MCPTarget) UnmarshalJSON(b []byte) error { return ResourceUnmarshaler.Unmarshal(bytes.NewReader(b), this) } +// MarshalJSON is a custom marshaler for MCPTarget_ResponseCompression +func (this *MCPTarget_ResponseCompression) MarshalJSON() ([]byte, error) { + str, err := ResourceMarshaler.MarshalToString(this) + return []byte(str), err +} + +// UnmarshalJSON is a custom unmarshaler for MCPTarget_ResponseCompression +func (this *MCPTarget_ResponseCompression) UnmarshalJSON(b []byte) error { + return ResourceUnmarshaler.Unmarshal(bytes.NewReader(b), this) +} + // MarshalJSON is a custom marshaler for BackendReference func (this *BackendReference) MarshalJSON() ([]byte, error) { str, err := ResourceMarshaler.MarshalToString(this) diff --git a/architecture/README.md b/architecture/README.md index 12293a1ea..db1bdb2dc 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -4,4 +4,5 @@ This folder contains developer-facing documentation on the project architecture. Recommended reading order: 1. [Configuration](configuration.md) -1. [CEL](cel.md) \ No newline at end of file +1. [CEL](cel.md) +1. [MCP Response Compression](mcp-response-compression.md) \ No newline at end of file diff --git a/architecture/mcp-response-compression.md b/architecture/mcp-response-compression.md new file mode 100644 index 000000000..221de1986 --- /dev/null +++ b/architecture/mcp-response-compression.md @@ -0,0 +1,74 @@ +# MCP Response Compression + +MCP tool call responses frequently contain large JSON payloads — lists of database rows, API results, file metadata — that consume significant context window tokens when consumed by LLM-based agents. Response compression converts these JSON payloads into compact tabular formats (markdown, TSV, CSV) at the proxy layer, reducing token usage while preserving the tabular structure. Note that nested objects are summarized as `{...}` and large arrays are truncated, so this is a lossy transformation optimized for LLM consumption rather than lossless encoding. + +This document covers the design and architecture of the compression pipeline. + +## Design Decisions + +### Why at the proxy layer + +Compression could happen at the MCP server, the client, or the proxy. Doing it at the proxy has several advantages: + +* **No upstream changes required.** MCP servers return standard JSON; compression is transparent. +* **Per-target configuration.** Different backends may benefit from different formats — a data-heavy API might use TSV while a human-readable tool uses markdown. +* **Consistent behavior.** All clients benefit without each needing its own compression logic. + +The tradeoff is that the proxy must parse and re-serialize JSON, adding latency proportional to response size. In practice this is small relative to the upstream call and LLM processing time. + +### Format selection + +The three formats target different consumption patterns: + +* **Markdown** — best for LLMs that handle markdown well (most do). Preserves readability. +* **TSV** — minimal overhead, no escaping needed for most data. Good for structured pipelines. +* **CSV** — standard interchange format with proper escaping. Useful when downstream tooling expects CSV. + +The `none` default means compression is opt-in; existing behavior is unchanged. + +### What gets compressed + +Compression targets `CallToolResult` messages containing text content that parses as JSON. The converter handles three shapes: + +* **Arrays of objects** — the common case (e.g., database query results). Each object becomes a row, keys become column headers. +* **Wrapper objects with array fields** — scalar fields are preserved as header lines above the table, array fields are rendered as tables. This handles paginated API responses that wrap results in metadata. +* **Nested values** — arrays of 5 or fewer items are shown inline; larger arrays show the first 5 items plus a count. Nested objects display as `{...}`. + +Non-tabular JSON (scalars, deeply nested structures) passes through unchanged. This is intentional — forcing non-tabular data into a table would lose information. + +## Architecture + +### Configuration flow + +Response compression follows the same configuration pattern as other per-target settings: + +1. **CRD** — `responseCompression` on [`AgentgatewayBackend`](../controller/api/v1alpha1/agentgateway/agentgateway_backend_types.go) MCP targets, with `enabled` (bool) and `format` (string) fields. +2. **Controller** — [`translate.go`](../controller/pkg/syncer/backend/translate.go) maps the CRD field into the xDS [`MCPTarget.ResponseCompression`](../crates/protos/proto/resource.proto) proto message. +3. **xDS → IR** — [`agent_xds.rs`](../crates/agentgateway/src/types/agent_xds.rs) converts the proto format string to the internal `CompressionFormat` enum on [`McpTarget`](../crates/agentgateway/src/types/agent.rs). + +This maintains the project's design philosophy of nearly direct CRD → xDS → IR mappings. + +### Runtime pipeline + +The compression module lives in [`mcp/compress.rs`](../crates/agentgateway/src/mcp/compress.rs). At runtime: + +1. The MCP handler in [`handler.rs`](../crates/agentgateway/src/mcp/handler.rs) checks the target's `response_compression` field. +2. If enabled, `compress_stream()` wraps the upstream response stream. For each `ServerJsonRpcMessage` containing a `CallToolResult` with text content, it calls `compress_response()`. +3. `compress_response()` attempts JSON parsing. If the content is valid JSON with tabular structure, it converts to the target format. Otherwise the content passes through unchanged. +4. Metrics are recorded for each attempt — see the [metrics section](#metrics) below. + +The stream wrapping approach means compression happens inline without buffering the entire response, though individual tool call results are fully parsed. + +### Metrics + +Compression exposes Prometheus metrics through the standard agentgateway metrics registry in [`telemetry/metrics.rs`](../crates/agentgateway/src/telemetry/metrics.rs): + +* `mcp_response_compression_total` / `mcp_response_compression_skipped_total` — counts of compressed vs. skipped responses. +* `mcp_response_compression_original_bytes` / `mcp_response_compression_compressed_bytes` — size histograms. +* `mcp_response_compression_ratio` — compression ratio (0.0–1.0). + +All metrics carry `target` and `format` labels. + +## Testing + +Unit tests in [`compress_tests.rs`](../crates/agentgateway/src/mcp/compress_tests.rs) cover the core conversion logic: arrays of objects, nested values, wrapper objects, and non-convertible inputs. Integration with the handler is tested through the existing MCP test infrastructure in [`mcp_tests.rs`](../crates/agentgateway/src/mcp/mcp_tests.rs). diff --git a/controller/api/v1alpha1/agentgateway/agentgateway_backend_types.go b/controller/api/v1alpha1/agentgateway/agentgateway_backend_types.go index bf90fa1f7..792925cc2 100644 --- a/controller/api/v1alpha1/agentgateway/agentgateway_backend_types.go +++ b/controller/api/v1alpha1/agentgateway/agentgateway_backend_types.go @@ -444,6 +444,21 @@ type McpTargetSelector struct { // instead. // +optional Static *McpTarget `json:"static,omitempty"` + + // ResponseCompression configures response compression for the target. + // +optional + ResponseCompression *ResponseCompression `json:"responseCompression,omitempty"` +} + +// ResponseCompression configures response compression. +type ResponseCompression struct { + // Enabled determines if response compression is enabled. + // +optional + Enabled bool `json:"enabled,omitempty"` + + // Format specifies the format to use for compression (e.g., markdown). + // +optional + Format string `json:"format,omitempty"` } const ( diff --git a/controller/api/v1alpha1/agentgateway/zz_generated.deepcopy.go b/controller/api/v1alpha1/agentgateway/zz_generated.deepcopy.go index cdd5059ff..6ac60cfec 100644 --- a/controller/api/v1alpha1/agentgateway/zz_generated.deepcopy.go +++ b/controller/api/v1alpha1/agentgateway/zz_generated.deepcopy.go @@ -2243,6 +2243,11 @@ func (in *McpTargetSelector) DeepCopyInto(out *McpTargetSelector) { *out = new(McpTarget) (*in).DeepCopyInto(*out) } + if in.ResponseCompression != nil { + in, out := &in.ResponseCompression, &out.ResponseCompression + *out = new(ResponseCompression) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new McpTargetSelector. @@ -2614,6 +2619,21 @@ func (in *ResourceAdd) DeepCopy() *ResourceAdd { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseCompression) DeepCopyInto(out *ResponseCompression) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseCompression. +func (in *ResponseCompression) DeepCopy() *ResponseCompression { + if in == nil { + return nil + } + out := new(ResponseCompression) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Retry) DeepCopyInto(out *Retry) { *out = *in diff --git a/controller/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml b/controller/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml index c63e3ffb8..8e5d0268d 100644 --- a/controller/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml +++ b/controller/install/helm/agentgateway-crds/templates/agentgateway.dev_agentgatewaybackends.yaml @@ -6764,6 +6764,19 @@ spec: minLength: 1 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string + responseCompression: + description: ResponseCompression configures response compression + for the target. + properties: + enabled: + description: Enabled determines if response compression + is enabled. + type: boolean + format: + description: Format specifies the format to use for + compression (e.g., markdown). + type: string + type: object selector: description: |- `selector` is the label selector used to select `Service` resources. diff --git a/controller/pkg/syncer/backend/translate.go b/controller/pkg/syncer/backend/translate.go index 8ff33f491..68d76938c 100644 --- a/controller/pkg/syncer/backend/translate.go +++ b/controller/pkg/syncer/backend/translate.go @@ -295,12 +295,28 @@ func TranslateMCPBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBa mcpTarget.Protocol = api.MCPTarget_STREAMABLE_HTTP } + if rc := target.ResponseCompression; rc != nil && rc.Enabled { + mcpTarget.ResponseCompression = &api.MCPTarget_ResponseCompression{ + Enabled: rc.Enabled, + Format: rc.Format, + } + } + mcpTargets = append(mcpTargets, mcpTarget) } else if s := target.Selector; s != nil { targets, err := TranslateMCPSelectorTargets(ctx, be.Namespace, target.Selector) if err != nil { return nil, err } + // Apply responseCompression from the target selector to each generated target + if rc := target.ResponseCompression; rc != nil && rc.Enabled { + for _, t := range targets { + t.ResponseCompression = &api.MCPTarget_ResponseCompression{ + Enabled: rc.Enabled, + Format: rc.Format, + } + } + } mcpTargets = append(mcpTargets, targets...) } } diff --git a/crates/agentgateway/src/http/oidc/tests.rs b/crates/agentgateway/src/http/oidc/tests.rs index 707c9349c..ee1da8931 100644 --- a/crates/agentgateway/src/http/oidc/tests.rs +++ b/crates/agentgateway/src/http/oidc/tests.rs @@ -54,6 +54,7 @@ fn policy_client() -> crate::proxy::httpproxy::PolicyClient { let proxy = setup_proxy_test("{}").expect("proxy test harness"); crate::proxy::httpproxy::PolicyClient { inputs: proxy.inputs(), + span_writer: Default::default(), } } diff --git a/crates/agentgateway/src/llm/mod.rs b/crates/agentgateway/src/llm/mod.rs index b082c4d35..09696efec 100644 --- a/crates/agentgateway/src/llm/mod.rs +++ b/crates/agentgateway/src/llm/mod.rs @@ -819,8 +819,9 @@ impl AIProvider { if original_format.supports_prompt_guard() { let http_headers = &parts.headers; let claims = parts.extensions.get::().cloned(); + let sw = log.as_ref().map(|l| l.span_writer()).unwrap_or_default(); if let Some(dr) = p - .apply_prompt_guard(backend_info, &mut req, http_headers, claims) + .apply_prompt_guard(backend_info, &mut req, http_headers, claims, sw) .await .map_err(|e| { warn!("failed to call prompt guard webhook: {e}"); diff --git a/crates/agentgateway/src/llm/policy/mod.rs b/crates/agentgateway/src/llm/policy/mod.rs index bc34db389..5ee764079 100644 --- a/crates/agentgateway/src/llm/policy/mod.rs +++ b/crates/agentgateway/src/llm/policy/mod.rs @@ -369,9 +369,11 @@ impl Policy { req: &mut dyn RequestType, http_headers: &HeaderMap, claims: Option, + span_writer: crate::telemetry::log::SpanWriter, ) -> anyhow::Result> { let client = PolicyClient { inputs: backend_info.inputs.clone(), + span_writer, }; for g in self .prompt_guard @@ -887,6 +889,9 @@ impl Policy { phase: crate::telemetry::metrics::GuardrailPhase, action: crate::telemetry::metrics::GuardrailAction, ) { + let _span = client + .span_writer + .start(format!("guardrail:{phase:?}:{action:?}")); client .inputs .metrics diff --git a/crates/agentgateway/src/llm/tests.rs b/crates/agentgateway/src/llm/tests.rs index 6b1cc1ecf..39d11ec76 100644 --- a/crates/agentgateway/src/llm/tests.rs +++ b/crates/agentgateway/src/llm/tests.rs @@ -977,6 +977,7 @@ async fn process_response_routes_streaming_error_to_buffered_path() { let client = PolicyClient { inputs: setup_proxy_test("{}").unwrap().pi, + span_writer: Default::default(), }; let result = bedrock diff --git a/crates/agentgateway/src/mcp/compress.rs b/crates/agentgateway/src/mcp/compress.rs new file mode 100644 index 000000000..8f7713b47 --- /dev/null +++ b/crates/agentgateway/src/mcp/compress.rs @@ -0,0 +1,391 @@ +#[cfg(feature = "schema")] +use schemars::JsonSchema; + +use serde_json::Value; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase")] +pub enum CompressionFormat { + Markdown, + Tsv, + Csv, + None, +} + +impl std::fmt::Display for CompressionFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CompressionFormat::Markdown => write!(f, "markdown"), + CompressionFormat::Tsv => write!(f, "tsv"), + CompressionFormat::Csv => write!(f, "csv"), + CompressionFormat::None => write!(f, "none"), + } + } +} + +/// Entry point for response compression +pub fn compress_response(content: &str, format: CompressionFormat) -> Option { + if format == CompressionFormat::None { + return None; + } + + let json: Value = serde_json::from_str(content).ok()?; + + match json { + // Direct array of objects + Value::Array(arr) => { + if arr.iter().all(|v| v.is_object()) && !arr.is_empty() { + Some(convert_array_to_table(&arr, format)) + } else { + None + } + }, + // Object with array field + Value::Object(ref obj) => { + // Look for a top-level key whose value is an array of objects + for (key, value) in obj.iter() { + if let Value::Array(arr) = value + && arr.iter().all(|v| v.is_object()) + && !arr.is_empty() + { + let mut result = String::new(); + + // Add scalar fields as header lines + for (k, v) in obj.iter() { + if k != key && !v.is_array() { + result.push_str(&format!("{}: {}\n", k, render_value(v))); + } + } + if !result.is_empty() { + result.push('\n'); + } + + result.push_str(&convert_array_to_table(arr, format)); + return Some(result); + } + } + None + }, + _ => None, + } +} + +fn convert_array_to_table(arr: &[Value], format: CompressionFormat) -> String { + if arr.is_empty() { + return String::new(); + } + + // Collect all unique keys from all objects + let mut all_keys = Vec::new(); + let mut key_set = std::collections::HashSet::new(); + + // Use keys from first object to maintain order + if let Some(Value::Object(first_obj)) = arr.first() { + for key in first_obj.keys() { + all_keys.push(key.clone()); + key_set.insert(key.clone()); + } + } + + // Add any additional keys from other objects + for item in arr.iter() { + if let Value::Object(obj) = item { + for key in obj.keys() { + if !key_set.contains(key) { + all_keys.push(key.clone()); + key_set.insert(key.clone()); + } + } + } + } + + match format { + CompressionFormat::Markdown => { + let mut result = String::new(); + + // Header row + result.push_str("| "); + result.push_str( + &all_keys + .iter() + .map(|k| escape_markdown(k)) + .collect::>() + .join(" | "), + ); + result.push_str(" |\n"); + + // Separator row + result.push('|'); + for _ in &all_keys { + result.push_str("------|"); + } + result.push('\n'); + + // Data rows + for item in arr { + if let Value::Object(obj) = item { + result.push_str("| "); + let values: Vec = all_keys + .iter() + .map(|key| escape_markdown(&obj.get(key).map(render_value).unwrap_or_default())) + .collect(); + result.push_str(&values.join(" | ")); + result.push_str(" |\n"); + } + } + + result + }, + CompressionFormat::Tsv => { + let mut result = String::new(); + + // Header row + result.push_str( + &all_keys + .iter() + .map(|k| escape_tsv(k)) + .collect::>() + .join("\t"), + ); + result.push('\n'); + + // Data rows + for item in arr { + if let Value::Object(obj) = item { + let values: Vec = all_keys + .iter() + .map(|key| escape_tsv(&obj.get(key).map(render_value).unwrap_or_default())) + .collect(); + result.push_str(&values.join("\t")); + result.push('\n'); + } + } + + result + }, + CompressionFormat::Csv => { + let mut result = String::new(); + + // Header row + result.push_str(&all_keys.join(",")); + result.push('\n'); + + // Data rows + for item in arr { + if let Value::Object(obj) = item { + let values: Vec = all_keys + .iter() + .map(|key| { + obj + .get(key) + .map(|v| escape_csv_value(&render_value(v))) + .unwrap_or_default() + }) + .collect(); + result.push_str(&values.join(",")); + result.push('\n'); + } + } + + result + }, + CompressionFormat::None => String::new(), + } +} + +fn render_value(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => { + if *b { + "true".to_string() + } else { + "false".to_string() + } + }, + Value::Null => String::new(), + Value::Array(arr) => { + if arr.len() <= 5 { + // Render flat arrays as comma-separated if all elements are primitives + if arr.iter().all(|v| !v.is_object() && !v.is_array()) { + arr.iter().map(render_value).collect::>().join(",") + } else if arr.iter().all(|v| v.is_object()) { + format!("[{{...}} x {}]", arr.len()) + } else { + "{...}".to_string() + } + } else { + // For arrays > 5 items, show first 5 + count + if arr.iter().all(|v| !v.is_object() && !v.is_array()) { + let first_five: Vec = arr.iter().take(5).map(render_value).collect(); + format!("{} (+{} more)", first_five.join(","), arr.len() - 5) + } else if arr.iter().all(|v| v.is_object()) { + format!("[{{...}} x {}]", arr.len()) + } else { + "{...}".to_string() + } + } + }, + Value::Object(_) => "{...}".to_string(), + } +} + +fn escape_markdown(value: &str) -> String { + value.replace('|', "\\|").replace('\n', " ") +} + +fn escape_tsv(value: &str) -> String { + value.replace(['\t', '\n'], " ") +} + +fn escape_csv_value(value: &str) -> String { + if value.contains(',') || value.contains('"') || value.contains('\n') { + format!("\"{}\"", value.replace('"', "\"\"")) + } else { + value.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_json_array_to_markdown() { + let input = r#"[ + {"name": "svc-a", "namespace": "default", "status": "Running"}, + {"name": "svc-b", "namespace": "default", "status": "Pending"} + ]"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("| name | namespace | status |")); + assert!(output.contains("| svc-a | default | Running |")); + assert!(output.contains("| svc-b | default | Pending |")); + } + + #[test] + fn test_json_array_to_tsv() { + let input = r#"[ + {"name": "svc-a", "namespace": "default", "status": "Running"}, + {"name": "svc-b", "namespace": "default", "status": "Pending"} + ]"#; + + let result = compress_response(input, CompressionFormat::Tsv); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("name\tnamespace\tstatus")); + assert!(output.contains("svc-a\tdefault\tRunning")); + } + + #[test] + fn test_json_array_to_csv() { + let input = r#"[ + {"name": "svc-a", "namespace": "default", "status": "Running"}, + {"name": "svc-b", "namespace": "default", "status": "Pending"} + ]"#; + + let result = compress_response(input, CompressionFormat::Csv); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("name,namespace,status")); + assert!(output.contains("svc-a,default,Running")); + } + + #[test] + fn test_wrapper_object() { + let input = r#"{ + "items": [ + {"name": "svc-a", "status": "Running"}, + {"name": "svc-b", "status": "Pending"} + ], + "count": 2 + }"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("count: 2")); + assert!(output.contains("| name | status |")); + } + + #[test] + fn test_non_convertible_passthrough() { + let input = r#"{"message": "hello world"}"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_none()); + } + + #[test] + fn test_array_value_rendering() { + let input = r#"[ + {"name": "test", "tags": ["a", "b", "c"]}, + {"name": "test2", "tags": ["x", "y", "z", "w", "v", "u", "t"]} + ]"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("a,b,c")); + assert!(output.contains("x,y,z,w,v (+2 more)")); + } + + #[test] + fn test_nested_objects() { + let input = r#"[ + {"name": "test", "metadata": {"created": "2023-01-01"}}, + {"name": "test2", "metadata": {"created": "2023-01-02"}} + ]"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("{...}")); + } + + #[test] + fn test_empty_array() { + let input = r#"[]"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_none()); + } + + #[test] + fn test_markdown_escapes_pipe() { + let input = r#"[{"name": "a|b", "value": "ok"}]"#; + let result = compress_response(input, CompressionFormat::Markdown).unwrap(); + assert!(result.contains("a\\|b")); + assert!(!result.contains("| a|b |")); + } + + #[test] + fn test_tsv_escapes_tab_and_newline() { + let input = r#"[{"name": "a\tb", "desc": "line1\nline2"}]"#; + let result = compress_response(input, CompressionFormat::Tsv).unwrap(); + // Tabs and newlines in values should be replaced with spaces + assert!(!result.contains("a\tb")); + } + + #[test] + fn test_single_object() { + let input = r#"[{"name": "single", "status": "ok"}]"#; + + let result = compress_response(input, CompressionFormat::Markdown); + assert!(result.is_some()); + + let output = result.unwrap(); + assert!(output.contains("| name | status |")); + assert!(output.contains("| single | ok |")); + } +} diff --git a/crates/agentgateway/src/mcp/handler.rs b/crates/agentgateway/src/mcp/handler.rs index c5bf3da19..e624abf7a 100644 --- a/crates/agentgateway/src/mcp/handler.rs +++ b/crates/agentgateway/src/mcp/handler.rs @@ -23,10 +23,13 @@ use rmcp::ErrorData; use rmcp::model::{ ClientNotification, ClientRequest, Implementation, JsonRpcNotification, JsonRpcRequest, ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, ListToolsResult, - ProtocolVersion, RequestId, ServerCapabilities, ServerInfo, ServerJsonRpcMessage, ServerResult, + ProtocolVersion, RawContent, RequestId, ServerCapabilities, ServerInfo, ServerJsonRpcMessage, + ServerResult, }; use tracing::{debug, warn}; +use crate::mcp::compress::{CompressionFormat, compress_response}; + const DELIMITER: &str = "_"; fn resource_name(default_target_name: Option<&String>, target: &str, name: &str) -> String { @@ -41,17 +44,19 @@ fn resource_name(default_target_name: Option<&String>, target: &str, name: &str) pub struct Relay { upstreams: Arc, pub policies: McpAuthorizationSet, + pub metrics: Arc, } pub struct RelayInputs { pub backend: McpBackendGroup, pub policies: McpAuthorizationSet, pub client: PolicyClient, + pub metrics: Arc, } impl RelayInputs { pub fn build_new_connections(self) -> Result { - Relay::new(self.backend, self.policies, self.client) + Relay::new(self.backend, self.policies, self.client, self.metrics) } } @@ -60,16 +65,19 @@ impl Relay { backend: McpBackendGroup, policies: McpAuthorizationSet, client: PolicyClient, + metrics: Arc, ) -> Result { Ok(Self { upstreams: Arc::new(upstream::UpstreamGroup::new(client, backend)?), policies, + metrics, }) } pub fn with_policies(&self, policies: McpAuthorizationSet) -> Self { Self { upstreams: self.upstreams.clone(), policies, + metrics: self.metrics.clone(), } } @@ -363,6 +371,13 @@ impl Relay { ))); }; let stream = us.generic_stream(r, &ctx).await?; + let compression_format = self.upstreams.get_compression_format(service_name); + let stream = compress_stream( + stream, + compression_format, + self.metrics.clone(), + service_name.to_string(), + ); messages_to_response(id, stream, mcp_log) } @@ -569,6 +584,80 @@ pub fn setup_request_log( (_span, log, cel) } +fn compress_stream( + stream: impl Stream> + Send + 'static, + compression_format: Option, + metrics: Arc, + target_name: String, +) -> impl Stream> + Send + 'static { + use agent_core::metrics::DefaultedUnknown; + use futures_util::StreamExt; + use rmcp::model::{CallToolResult, ServerJsonRpcMessage, ServerResult}; + + stream.map(move |result| { + let Ok(mut message) = result else { + return result; + }; + + let Some(format) = compression_format else { + return Ok(message); + }; + + // Only compress CallToolResult messages + if let ServerJsonRpcMessage::Response(response) = &mut message + && let ServerResult::CallToolResult(CallToolResult { content, .. }) = &mut response.result + { + let labels = crate::telemetry::metrics::CompressionLabels { + target: DefaultedUnknown::from(Some(agent_core::strng::new(&target_name))), + format: DefaultedUnknown::from(Some(format)), + }; + + for content_item in content.iter_mut() { + if let RawContent::Text(text_content) = &mut content_item.raw { + let original_len = text_content.text.len() as f64; + if let Some(compressed) = compress_response(&text_content.text, format) { + let compressed_len = compressed.len() as f64; + let ratio = if original_len > 0. { + compressed_len / original_len + } else { + 1.0 + }; + + metrics + .mcp_response_compression_original_bytes + .get_or_create(&labels) + .observe(original_len); + metrics + .mcp_response_compression_compressed_bytes + .get_or_create(&labels) + .observe(compressed_len); + metrics + .mcp_response_compression_ratio + .get_or_create(&labels) + .observe(ratio); + metrics + .mcp_response_compression_total + .get_or_create(&labels) + .inc(); + + // Only apply compression if the result is actually smaller + if compressed_len < original_len { + text_content.text = compressed; + } + } else { + metrics + .mcp_response_compression_skipped_total + .get_or_create(&labels) + .inc(); + } + } + } + } + + Ok(message) + }) +} + fn messages_to_response( id: RequestId, stream: impl Stream> + Send + 'static, diff --git a/crates/agentgateway/src/mcp/mcp_tests.rs b/crates/agentgateway/src/mcp/mcp_tests.rs index 3ec072ac8..88e177667 100644 --- a/crates/agentgateway/src/mcp/mcp_tests.rs +++ b/crates/agentgateway/src/mcp/mcp_tests.rs @@ -216,6 +216,7 @@ async fn stateless_multiplex_get_prompt_initializes_only_target() { async fn stateless_multiplex_delete_session_skips_uninitialized_targets() { let mock_a = mock_streamable_http_server(true).await; let mock_b = mock_streamable_http_server(true).await; + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![ @@ -227,8 +228,10 @@ async fn stateless_multiplex_delete_session_skips_uninitialized_targets() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); let session_manager = @@ -1616,6 +1619,7 @@ async fn test_zero_targets_fail_closed() { }; let client = PolicyClient { inputs: setup_proxy_test("{}").unwrap().pi, + span_writer: Default::default(), }; let err = crate::mcp::upstream::UpstreamGroup::new(client, backend).unwrap_err(); assert!(matches!(err, crate::mcp::Error::NoBackends)); @@ -1630,6 +1634,7 @@ async fn test_zero_targets_fail_open() { }; let client = PolicyClient { inputs: setup_proxy_test("{}").unwrap().pi, + span_writer: Default::default(), }; crate::mcp::upstream::UpstreamGroup::new(client, backend).unwrap(); } @@ -1667,6 +1672,7 @@ async fn test_setup_partial_success_fail_open() { }; let client = PolicyClient { inputs: setup_proxy_test("{}").unwrap().pi, + span_writer: Default::default(), }; let group = crate::mcp::upstream::UpstreamGroup::new(client, backend).unwrap(); assert_eq!(group.size(), 1); @@ -1704,6 +1710,7 @@ async fn test_all_targets_fail_open_still_errors() { }; let client = PolicyClient { inputs: setup_proxy_test("{}").unwrap().pi, + span_writer: Default::default(), }; let err = crate::mcp::upstream::UpstreamGroup::new(client, backend).unwrap_err(); assert!(matches!(err, crate::mcp::Error::NoBackends)); @@ -1717,6 +1724,7 @@ fn fake_streamable_target(name: &str, addr: SocketAddr) -> Arc { "/unused-{name}" )), path: "/mcp".to_string(), + response_compression: None, }), backend_policies: Default::default(), backend: Some(crate::types::agent::SimpleBackend::Opaque( @@ -1735,6 +1743,7 @@ fn fake_sse_target(name: &str, addr: SocketAddr) -> Arc { "/unused-{name}" )), path: "/sse".to_string(), + response_compression: None, }), backend_policies: Default::default(), backend: Some(crate::types::agent::SimpleBackend::Opaque( @@ -1763,6 +1772,7 @@ fn fake_openapi_target(name: &str, addr: SocketAddr) -> Arc { "/unused-{name}" )), schema: Arc::new(schema), + response_compression: None, }), backend_policies: Default::default(), backend: Some(crate::types::agent::SimpleBackend::Opaque( @@ -1816,6 +1826,7 @@ fn persisted_stateless_session( #[test] fn test_openapi_targets_emit_stateless_session_state() { + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![fake_openapi_target( @@ -1827,8 +1838,10 @@ fn test_openapi_targets_emit_stateless_session_state() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -1864,6 +1877,7 @@ fn test_openapi_targets_emit_stateless_session_state() { #[test] fn test_sse_targets_emit_stateless_session_state() { + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![fake_sse_target( @@ -1875,8 +1889,10 @@ fn test_sse_targets_emit_stateless_session_state() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -1912,6 +1928,7 @@ fn test_sse_targets_emit_stateless_session_state() { #[tokio::test] async fn test_stdio_targets_remain_non_stateless() { + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![fake_stdio_target("stdio")], @@ -1920,8 +1937,10 @@ async fn test_stdio_targets_remain_non_stateless() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -1932,6 +1951,7 @@ async fn test_stdio_targets_remain_non_stateless() { async fn test_fanout_deletion_fail_open_skips_failed_upstreams() { let good = mock_streamable_http_server(true).await; let bad_addr = SocketAddr::from(([127, 0, 0, 1], 31999)); + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![ @@ -1943,8 +1963,10 @@ async fn test_fanout_deletion_fail_open_skips_failed_upstreams() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -1965,6 +1987,7 @@ async fn test_fanout_deletion_fail_open_skips_failed_upstreams() { #[test] fn test_set_sessions_matches_by_target_name() { + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![ @@ -1976,8 +1999,10 @@ fn test_set_sessions_matches_by_target_name() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -2014,6 +2039,7 @@ fn test_set_sessions_matches_by_target_name() { #[test] fn test_set_sessions_rejects_mismatched_target_set() { + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![ @@ -2025,8 +2051,10 @@ fn test_set_sessions_rejects_mismatched_target_set() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -2058,6 +2086,7 @@ fn test_merge_initialize_merges_upstream_instructions_when_multiplexing() { Implementation, InitializeResult, ProtocolVersion, ServerCapabilities, ServerResult, }; + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![ @@ -2069,8 +2098,10 @@ fn test_merge_initialize_merges_upstream_instructions_when_multiplexing() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -2132,6 +2163,7 @@ fn test_merge_initialize_no_instructions_when_multiplexing() { Implementation, InitializeResult, ProtocolVersion, ServerCapabilities, ServerResult, }; + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![fake_streamable_target( @@ -2143,8 +2175,10 @@ fn test_merge_initialize_no_instructions_when_multiplexing() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); @@ -2183,6 +2217,7 @@ fn test_merge_initialize_forwards_single_backend_without_multiplexing() { Implementation, InitializeResult, ProtocolVersion, ServerCapabilities, ServerResult, }; + let test_pi = setup_proxy_test("{}").unwrap().pi; let relay = Relay::new( McpBackendGroup { targets: vec![fake_streamable_target( @@ -2194,8 +2229,10 @@ fn test_merge_initialize_forwards_single_backend_without_multiplexing() { }, empty_mcp_policies(), PolicyClient { - inputs: setup_proxy_test("{}").unwrap().pi, + inputs: test_pi.clone(), + span_writer: Default::default(), }, + test_pi.metrics.clone(), ) .unwrap(); diff --git a/crates/agentgateway/src/mcp/mod.rs b/crates/agentgateway/src/mcp/mod.rs index 696d55d51..5c67731e9 100644 --- a/crates/agentgateway/src/mcp/mod.rs +++ b/crates/agentgateway/src/mcp/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod auth; +pub mod compress; mod handler; mod mergestream; mod rbac; diff --git a/crates/agentgateway/src/mcp/router.rs b/crates/agentgateway/src/mcp/router.rs index 3af1b20b1..51d86453a 100644 --- a/crates/agentgateway/src/mcp/router.rs +++ b/crates/agentgateway/src/mcp/router.rs @@ -105,7 +105,10 @@ impl App { } }; let sm = self.session.clone(); - let client = PolicyClient { inputs: pi.clone() }; + let client = PolicyClient { + inputs: pi.clone(), + span_writer: log.span_writer(), + }; let authorization_policies = backend_policies .mcp_authorization .unwrap_or_else(|| McpAuthorizationSet::new(RuleSets::from(Vec::new()))); @@ -143,6 +146,7 @@ impl App { backend: backends.clone(), policies: authorization_policies.clone(), client: client.clone(), + metrics: pi.metrics.clone(), }, )) .await @@ -159,6 +163,7 @@ impl App { backend: backends.clone(), policies: authorization_policies.clone(), client: client.clone(), + metrics: pi.metrics.clone(), }, )) .await diff --git a/crates/agentgateway/src/mcp/upstream/mod.rs b/crates/agentgateway/src/mcp/upstream/mod.rs index c7025c1c4..d0196bf37 100644 --- a/crates/agentgateway/src/mcp/upstream/mod.rs +++ b/crates/agentgateway/src/mcp/upstream/mod.rs @@ -14,6 +14,7 @@ use thiserror::Error; use tokio::process::Command; use crate::mcp::FailureMode; +use crate::mcp::compress::CompressionFormat; use crate::mcp::mergestream::Messages; use crate::mcp::router::{McpBackendGroup, McpTarget}; use crate::mcp::streamablehttp::StreamableHttpPostResponse; @@ -377,4 +378,18 @@ impl UpstreamGroup { Ok(target) } + + pub fn get_compression_format(&self, service_name: &str) -> Option { + for tgt in &self.backend.targets { + if tgt.name.as_str() == service_name { + return match &tgt.spec { + McpTargetSpec::Sse(s) => s.response_compression, + McpTargetSpec::Mcp(s) => s.response_compression, + McpTargetSpec::OpenAPI(s) => s.response_compression, + McpTargetSpec::Stdio { .. } => None, + }; + } + } + None + } } diff --git a/crates/agentgateway/src/mcp/upstream/openapi/tests.rs b/crates/agentgateway/src/mcp/upstream/openapi/tests.rs index 122bd7f3f..5defc1621 100644 --- a/crates/agentgateway/src/mcp/upstream/openapi/tests.rs +++ b/crates/agentgateway/src/mcp/upstream/openapi/tests.rs @@ -54,7 +54,10 @@ async fn setup_with_prefix(prefix: &str) -> (MockServer, Handler) { mcp_state: mcp::router::App::new(stores.clone(), encoder), }); - let client = PolicyClient { inputs: pi.clone() }; + let client = PolicyClient { + inputs: pi.clone(), + span_writer: Default::default(), + }; let test_tool_get = Tool::new( Cow::Borrowed("get_user"), Cow::Borrowed("Get user details"), diff --git a/crates/agentgateway/src/proxy/httpproxy.rs b/crates/agentgateway/src/proxy/httpproxy.rs index f06d15b87..eebf5b0ab 100644 --- a/crates/agentgateway/src/proxy/httpproxy.rs +++ b/crates/agentgateway/src/proxy/httpproxy.rs @@ -1014,6 +1014,7 @@ impl HTTPProxy { fn policy_client(&self) -> PolicyClient { PolicyClient { inputs: self.inputs.clone(), + span_writer: Default::default(), } } } @@ -1277,6 +1278,7 @@ async fn make_backend_call( ) -> Result { let policy_client = PolicyClient { inputs: inputs.clone(), + span_writer: log.as_ref().map(|l| l.span_writer()).unwrap_or_default(), }; // The MCP backend aggregates multiple backends into a single backend. @@ -2231,6 +2233,7 @@ pub struct TunnelClient { #[derive(Debug, Clone)] pub struct PolicyClient { pub inputs: Arc, + pub span_writer: crate::telemetry::log::SpanWriter, } impl PolicyClient { diff --git a/crates/agentgateway/src/telemetry/log.rs b/crates/agentgateway/src/telemetry/log.rs index 7da23d274..e3f85406d 100644 --- a/crates/agentgateway/src/telemetry/log.rs +++ b/crates/agentgateway/src/telemetry/log.rs @@ -834,17 +834,24 @@ impl Drop for DropOnLog { .inc_by(retry_count as u64); } - Self::add_llm_metrics( - &log, - &route_identifier, - end_time, - duration, - llm_response.as_ref(), - &custom_metric_fields, - ); + if llm_response.is_some() { + let _llm_span = log.span_writer().start("llm"); + Self::add_llm_metrics( + &log, + &route_identifier, + end_time, + duration, + llm_response.as_ref(), + &custom_metric_fields, + ); + } if let Some(mcp) = &mcp && mcp.method_name.is_some() { + let _mcp_span = log.span_writer().start(format!( + "mcp:{}", + mcp.method_name.as_deref().unwrap_or("unknown") + )); // Check mcp.method_name is set, so we don't count things like GET and DELETE log .metrics @@ -1230,7 +1237,10 @@ impl PolicyGrpcLogExporter { let channel = GrpcReferenceChannel { target, policies: Arc::new(policies), - client: crate::proxy::httpproxy::PolicyClient { inputs }, + client: crate::proxy::httpproxy::PolicyClient { + inputs, + span_writer: Default::default(), + }, }; let tonic_client = opentelemetry_proto::tonic::collector::logs::v1::logs_service_client::LogsServiceClient::new( diff --git a/crates/agentgateway/src/telemetry/metrics.rs b/crates/agentgateway/src/telemetry/metrics.rs index d6377bd79..a1cfe006e 100644 --- a/crates/agentgateway/src/telemetry/metrics.rs +++ b/crates/agentgateway/src/telemetry/metrics.rs @@ -13,6 +13,7 @@ use prometheus_client::registry::{Metric, Registry, Unit}; use tracing::{debug, trace}; use crate::mcp::MCPOperation; +use crate::mcp::compress::CompressionFormat; use crate::proxy::ProxyResponseReason; use crate::types::agent::TransportProtocol; @@ -116,6 +117,12 @@ pub struct ConnectLabels { pub transport: DefaultedUnknown, } +#[derive(Clone, Hash, Debug, PartialEq, Eq, EncodeLabelSet)] +pub struct CompressionLabels { + pub target: DefaultedUnknown, + pub format: DefaultedUnknown>, +} + type Counter = Family; type Histogram = Family; type TCPCounter = Family; @@ -151,6 +158,16 @@ pub struct Metrics { // metrics for request retries pub retries: Counter, + + // MCP response compression metrics + pub mcp_response_compression_original_bytes: + Family, + pub mcp_response_compression_compressed_bytes: + Family, + pub mcp_response_compression_ratio: + Family, + pub mcp_response_compression_total: Family, + pub mcp_response_compression_skipped_total: Family, } // FilteredRegistry is a wrapper around Registry that allows to filter out certain metrics. @@ -366,6 +383,59 @@ impl Metrics { ); m }, + mcp_response_compression_original_bytes: { + let m = Family::::new_with_constructor(move || { + PromHistogram::new(COMPRESSION_BYTES_BUCKET) + }); + registry.register_with_unit( + "mcp_response_compression_original", + "Original response size before compression", + Unit::Bytes, + m.clone(), + ); + m + }, + mcp_response_compression_compressed_bytes: { + let m = Family::::new_with_constructor(move || { + PromHistogram::new(COMPRESSION_BYTES_BUCKET) + }); + registry.register_with_unit( + "mcp_response_compression_compressed", + "Response size after compression", + Unit::Bytes, + m.clone(), + ); + m + }, + mcp_response_compression_ratio: { + let m = Family::::new_with_constructor(move || { + PromHistogram::new(COMPRESSION_RATIO_BUCKET) + }); + registry.register( + "mcp_response_compression_ratio", + "Compression ratio (compressed / original)", + m.clone(), + ); + m + }, + mcp_response_compression_total: { + let m = Family::::default(); + registry.register( + "mcp_response_compression", + "Total number of MCP response compressions performed", + m.clone(), + ); + m + }, + mcp_response_compression_skipped_total: { + let m = Family::::default(); + registry.register( + "mcp_response_compression_skipped", + "Total number of MCP responses that did not qualify for compression", + m.clone(), + ); + m + }, retries: build( &mut registry, "retries", @@ -423,3 +493,10 @@ const OUTPUT_TOKEN_BUCKET: [f64; 14] = [ const FIRST_TOKEN_BUCKET: [f64; 16] = [ 0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, ]; +// Byte-size buckets for MCP response compression (256B to 16MB, exponential) +const COMPRESSION_BYTES_BUCKET: [f64; 12] = [ + 256., 1024., 4096., 16384., 65536., 262144., 524288., 1048576., 2097152., 4194304., 8388608., + 16777216., +]; +// Ratio buckets for compression ratio (0.0 = perfect compression, 1.0 = no compression) +const COMPRESSION_RATIO_BUCKET: [f64; 10] = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1.0]; diff --git a/crates/agentgateway/src/telemetry/trc.rs b/crates/agentgateway/src/telemetry/trc.rs index 2cedff4db..de68aede3 100644 --- a/crates/agentgateway/src/telemetry/trc.rs +++ b/crates/agentgateway/src/telemetry/trc.rs @@ -275,7 +275,10 @@ impl PolicyGrpcSpanExporter { let channel = GrpcReferenceChannel { target, policies: Arc::new(policies), - client: crate::proxy::httpproxy::PolicyClient { inputs }, + client: crate::proxy::httpproxy::PolicyClient { + inputs, + span_writer: Default::default(), + }, }; let tonic_client = opentelemetry_proto::tonic::collector::trace::v1::trace_service_client::TraceServiceClient::new( channel, diff --git a/crates/agentgateway/src/test_helpers/proxymock.rs b/crates/agentgateway/src/test_helpers/proxymock.rs index e8c3ac4d1..4928767c6 100644 --- a/crates/agentgateway/src/test_helpers/proxymock.rs +++ b/crates/agentgateway/src/test_helpers/proxymock.rs @@ -507,11 +507,13 @@ impl TestBind { McpTargetSpec::Mcp(StreamableHTTPTargetSpec { backend: sb, path: "/mcp".to_string(), + response_compression: None, }) } else { McpTargetSpec::Sse(SseTargetSpec { backend: sb, path: "/sse".to_string(), + response_compression: None, }) }, })], @@ -553,11 +555,13 @@ impl TestBind { McpTargetSpec::Mcp(StreamableHTTPTargetSpec { backend: sb, path: "/mcp".to_string(), + response_compression: None, }) } else { McpTargetSpec::Sse(SseTargetSpec { backend: sb, path: "/sse".to_string(), + response_compression: None, }) }, }) diff --git a/crates/agentgateway/src/types/agent.rs b/crates/agentgateway/src/types/agent.rs index da759ebf3..fd066c560 100644 --- a/crates/agentgateway/src/types/agent.rs +++ b/crates/agentgateway/src/types/agent.rs @@ -29,6 +29,7 @@ use crate::http::{ }; use crate::mcp::FailureMode; use crate::mcp::McpAuthorization; +use crate::mcp::compress::CompressionFormat; use crate::telemetry::log::OrderedStringMap; use crate::transport::tls; use crate::types::discovery::{NamespacedHostname, Service}; @@ -1263,6 +1264,8 @@ impl McpTargetSpec { pub struct SseTargetSpec { pub backend: SimpleBackendReference, pub path: String, + #[serde(default)] + pub response_compression: Option, } #[derive(Debug, Clone, serde::Serialize)] @@ -1271,6 +1274,8 @@ pub struct SseTargetSpec { pub struct StreamableHTTPTargetSpec { pub backend: SimpleBackendReference, pub path: String, + #[serde(default)] + pub response_compression: Option, } #[derive(Debug, Clone, serde::Serialize)] @@ -1281,6 +1286,8 @@ pub struct OpenAPITarget { #[serde(skip_serializing)] #[cfg_attr(feature = "schema", schemars(with = "serde_json::value::RawValue"))] pub schema: Arc, + #[serde(default)] + pub response_compression: Option, } #[derive(Debug, Clone, Default)] diff --git a/crates/agentgateway/src/types/agent_xds.rs b/crates/agentgateway/src/types/agent_xds.rs index 81b73259d..7ac95f16a 100644 --- a/crates/agentgateway/src/types/agent_xds.rs +++ b/crates/agentgateway/src/types/agent_xds.rs @@ -967,6 +967,17 @@ impl TryFrom<&proto::agent::McpTarget> for McpTarget { let proto = proto::agent::mcp_target::Protocol::try_from(s.protocol)?; let backend = resolve_simple_reference(s.backend.as_ref()); + let compression = s + .response_compression + .as_ref() + .filter(|rc| rc.enabled) + .and_then(|rc| match rc.format.as_str() { + "markdown" => Some(crate::mcp::compress::CompressionFormat::Markdown), + "tsv" => Some(crate::mcp::compress::CompressionFormat::Tsv), + "csv" => Some(crate::mcp::compress::CompressionFormat::Csv), + _ => None, // Unknown or "none" format with enabled=true treated as disabled + }); + Ok(Self { name: strng::new(&s.name), spec: match proto { @@ -977,6 +988,7 @@ impl TryFrom<&proto::agent::McpTarget> for McpTarget { } else { s.path.clone() }, + response_compression: compression, }), Protocol::Undefined | Protocol::StreamableHttp => { McpTargetSpec::Mcp(StreamableHTTPTargetSpec { @@ -986,6 +998,7 @@ impl TryFrom<&proto::agent::McpTarget> for McpTarget { } else { s.path.clone() }, + response_compression: compression, }) }, }, diff --git a/crates/agentgateway/src/types/local.rs b/crates/agentgateway/src/types/local.rs index 5b07e2bb2..440d9b2db 100644 --- a/crates/agentgateway/src/types/local.rs +++ b/crates/agentgateway/src/types/local.rs @@ -712,6 +712,7 @@ impl LocalBackend { McpTargetSpec::Sse(SseTargetSpec { backend: bref, path: path.clone(), + response_compression: None, }) }, LocalMcpTargetSpec::Mcp { backend } => { @@ -723,6 +724,7 @@ impl LocalBackend { McpTargetSpec::Mcp(StreamableHTTPTargetSpec { backend: bref, path: path.clone(), + response_compression: None, }) }, LocalMcpTargetSpec::Stdio { cmd, args, env } => McpTargetSpec::Stdio { cmd, args, env }, @@ -736,6 +738,7 @@ impl LocalBackend { McpTargetSpec::OpenAPI(OpenAPITarget { backend: bref, schema: openapi_schema.into(), + response_compression: None, }) }, }; diff --git a/crates/agentgateway/src/types/local_tests/mcp_normalized.snap b/crates/agentgateway/src/types/local_tests/mcp_normalized.snap index da97824e7..e0e70eb9e 100644 --- a/crates/agentgateway/src/types/local_tests/mcp_normalized.snap +++ b/crates/agentgateway/src/types/local_tests/mcp_normalized.snap @@ -66,6 +66,7 @@ backends: backend: backend: /mcp//ns/name/bind/3000/listener0/default/route0/backend0/1 path: /mcp + responseCompression: ~ name: remote stateful: true alwaysUsePrefix: false diff --git a/crates/protos/proto/resource.proto b/crates/protos/proto/resource.proto index 7743bd945..ec742f0b7 100644 --- a/crates/protos/proto/resource.proto +++ b/crates/protos/proto/resource.proto @@ -1295,10 +1295,15 @@ message MCPTarget { SSE = 1; STREAMABLE_HTTP = 2; } + message ResponseCompression { + bool enabled = 1; + string format = 2; + } string name = 1; BackendReference backend = 2; string path = 3; Protocol protocol = 4; + ResponseCompression response_compression = 5; } message BackendReference { diff --git a/schema/config.md b/schema/config.md index 37543f48b..7aa16f737 100644 --- a/schema/config.md +++ b/schema/config.md @@ -1223,6 +1223,9 @@ |`binds[].listeners[].routes[].backends[].mcp.targets[].openapi.schema.file`|string|| |`binds[].listeners[].routes[].backends[].mcp.targets[].openapi.schema.url`|string|| |`binds[].listeners[].routes[].backends[].mcp.targets[].name`|string|| +|`binds[].listeners[].routes[].backends[].mcp.targets[].responseCompression`|object|Configure response compression for this MCP target (Kubernetes CRD only).| +|`binds[].listeners[].routes[].backends[].mcp.targets[].responseCompression.enabled`|boolean|Whether response compression is enabled.| +|`binds[].listeners[].routes[].backends[].mcp.targets[].responseCompression.format`|string|Compression format: `markdown`, `tsv`, `csv`, or `none`.| |`binds[].listeners[].routes[].backends[].mcp.targets[].policies`|object|| |`binds[].listeners[].routes[].backends[].mcp.targets[].policies.requestHeaderModifier`|object|Headers to be modified in the request.| |`binds[].listeners[].routes[].backends[].mcp.targets[].policies.requestHeaderModifier.add`|object||