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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.6.70
v0.6.71
4 changes: 2 additions & 2 deletions charts/controller/crds/deployments.plural.sh_mcpservers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ spec:
protocol:
default: STREAMABLE_HTTP
description: |-
Protocol specifies the MCP transport protocol (SSE or STREAMABLE_HTTP).
STREAMABLE_HTTP is the modern protocol and is the default when omitted.
Protocol specifies the MCP transport protocol. Only STREAMABLE_HTTP is supported and it is the default
when omitted. SSE is no longer supported and will fail to synchronize.
enum:
- SSE
- STREAMABLE_HTTP
Expand Down
4 changes: 4 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]

# anubis' per-category `:logging` levels are overridden by hardcoded warning/error levels and by
# remote servers' own log notifications, so this is the only effective way to quiet it
config :anubis_mcp, log: false

config :phoenix, :json_library, JSON

config :console, Console.Guardian,
Expand Down
131 changes: 130 additions & 1 deletion go/client/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions go/cloud-query/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"crypto/tls"
"fmt"
"net"
"slices"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/encoding/gzip"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/reflection"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -74,6 +76,18 @@ func (in *Server) register() {
klog.V(log.LogLevelVerbose).InfoS("gRPC services registered", "count", len(in.services))
}

// compressResponses gzips unary responses for clients that advertise gzip in grpc-accept-encoding.
// grpc-go otherwise only compresses responses to compressed requests.
func compressResponses(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if supported, err := grpc.ClientSupportedCompressors(ctx); err == nil && slices.Contains(supported, gzip.Name) {
if err := grpc.SetSendCompressor(ctx, gzip.Name); err != nil {
klog.V(log.LogLevelExtended).InfoS("failed to enable response compression", "error", err)
}
}

return handler(ctx, req)
}

func (in *Server) init() (*Server, error) {
opts := []grpc.ServerOption{
grpc.KeepaliveParams(keepalive.ServerParameters{
Expand All @@ -88,6 +102,7 @@ func (in *Server) init() (*Server, error) {
MinTime: 10 * time.Second,
PermitWithoutStream: true,
}),
grpc.ChainUnaryInterceptor(compressResponses),
}

// Add TLS credentials if certificates are provided
Expand Down
4 changes: 2 additions & 2 deletions go/controller/api/v1alpha1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ type MCPServerSpec struct {
// +kubebuilder:validation:Optional
Name *string `json:"name,omitempty"`

// Protocol specifies the MCP transport protocol (SSE or STREAMABLE_HTTP).
// STREAMABLE_HTTP is the modern protocol and is the default when omitted.
// Protocol specifies the MCP transport protocol. Only STREAMABLE_HTTP is supported and it is the default
// when omitted. SSE is no longer supported and will fail to synchronize.
// +kubebuilder:validation:Optional
// +kubebuilder:default=STREAMABLE_HTTP
// +kubebuilder:validation:Enum=SSE;STREAMABLE_HTTP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ spec:
protocol:
default: STREAMABLE_HTTP
description: |-
Protocol specifies the MCP transport protocol (SSE or STREAMABLE_HTTP).
STREAMABLE_HTTP is the modern protocol and is the default when omitted.
Protocol specifies the MCP transport protocol. Only STREAMABLE_HTTP is supported and it is the default
when omitted. SSE is no longer supported and will fail to synchronize.
enum:
- SSE
- STREAMABLE_HTTP
Expand Down
2 changes: 1 addition & 1 deletion go/controller/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2617,7 +2617,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `name` _string_ | Name specifies the name for this MCP server.<br />If not provided, the name from the resource metadata will be used.<br />This name is used for identification and referencing in AI workflows. | | Optional: \{\} <br /> |
| `protocol` _[McpServerProtocol](#mcpserverprotocol)_ | Protocol specifies the MCP transport protocol (SSE or STREAMABLE_HTTP).<br />STREAMABLE_HTTP is the modern protocol and is the default when omitted. | STREAMABLE_HTTP | Enum: [SSE STREAMABLE_HTTP] <br />Optional: \{\} <br /> |
| `protocol` _[McpServerProtocol](#mcpserverprotocol)_ | Protocol specifies the MCP transport protocol. Only STREAMABLE_HTTP is supported and it is the default<br />when omitted. SSE is no longer supported and will fail to synchronize. | STREAMABLE_HTTP | Enum: [SSE STREAMABLE_HTTP] <br />Optional: \{\} <br /> |
| `url` _string_ | URL is the HTTP endpoint where the MCP server is hosted.<br />This must be a valid HTTP or HTTPS URL that the AI system can reach<br />to execute tool calls and interact with the server's capabilities. | | Required: \{\} <br /> |
| `bindings` _[Bindings](#bindings)_ | Bindings define the read and write access policies for this MCP server.<br />These control which users and groups can view, modify, or execute tools<br />provided by this server, enabling fine-grained access control. | | Optional: \{\} <br /> |
| `authentication` _[MCPServerAuthentication](#mcpserverauthentication)_ | Authentication specifies the authentication configuration for accessing this MCP server.<br />Different authentication methods are supported including built-in Plural JWT<br />and custom HTTP headers for integration with various authentication systems. | | Optional: \{\} <br /> |
Expand Down
14 changes: 14 additions & 0 deletions go/controller/internal/controller/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, r.handleDelete(ctx, mcpServer)
}

// A spec error can't resolve itself and the generation predicate re-triggers on the fix, so don't requeue.
if err := validateProtocol(mcpServer); err != nil {
logger.Error(err, "invalid MCP server spec")
utils.MarkCondition(mcpServer.SetCondition, v1alpha1.SynchronizedConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error())
return ctrl.Result{}, nil
}

attrs, err := r.Attributes(ctx, mcpServer)
if err != nil {
return common.HandleRequeue(nil, err, mcpServer.SetCondition)
Expand Down Expand Up @@ -106,6 +113,13 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return mcpServer.Spec.Reconciliation.Requeue(), nil
}

func validateProtocol(mcp *v1alpha1.MCPServer) error {
if mcp.Spec.Protocol != nil && *mcp.Spec.Protocol == console.McpServerProtocolSse {
return fmt.Errorf("the SSE protocol is no longer supported, set spec.protocol to %s or omit it", console.McpServerProtocolStreamableHTTP)
}
return nil
}

func (r *MCPServerReconciler) Attributes(ctx context.Context, mcp *v1alpha1.MCPServer) (*console.McpServerAttributes, error) {
protocol := console.McpServerProtocolStreamableHTTP
if mcp.Spec.Protocol != nil {
Expand Down
56 changes: 56 additions & 0 deletions go/controller/internal/controller/mcpserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/samber/lo"
"github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -158,4 +159,59 @@ var _ = Describe("MCP Server Controller", Ordered, func() {

})

Context("When reconciling a resource with the SSE protocol", func() {
const (
mcpServerName = "test-sse"
namespace = "default"
)

ctx := context.Background()
typeNamespacedName := types.NamespacedName{Name: mcpServerName, Namespace: namespace}

BeforeAll(func() {
Expect(common.MaybeCreate(k8sClient, &v1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{Name: mcpServerName, Namespace: namespace},
Spec: v1alpha1.MCPServerSpec{
URL: "test.url.com",
Protocol: lo.ToPtr(gqlclient.McpServerProtocolSse),
},
}, nil)).To(Succeed())
})

AfterAll(func() {
mcpServer := &v1alpha1.MCPServer{}
if err := k8sClient.Get(ctx, typeNamespacedName, mcpServer); err == nil {
Expect(k8sClient.Delete(ctx, mcpServer)).To(Succeed())
}
})

It("should mark the resource as failing to synchronize without upserting it", func() {
fakeConsoleClient := mocks.NewConsoleClientMock(mocks.TestingT)
fakeConsoleClient.On("UseCredentials", mock.Anything, mock.Anything).Return("", nil)

reconciler := &controller.MCPServerReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
ConsoleClient: fakeConsoleClient,
}

result, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(reconcile.Result{}))

mcpServer := &v1alpha1.MCPServer{}
Expect(k8sClient.Get(ctx, typeNamespacedName, mcpServer)).To(Succeed())
Expect(mcpServer.Status.ID).To(BeNil())

synced := meta.FindStatusCondition(mcpServer.Status.Conditions, v1alpha1.SynchronizedConditionType.String())
Expect(synced).NotTo(BeNil())
Expect(synced.Status).To(Equal(metav1.ConditionFalse))
Expect(synced.Reason).To(Equal(v1alpha1.SynchronizedConditionReasonError.String()))
Expect(synced.Message).To(ContainSubstring("SSE protocol is no longer supported"))

ready := meta.FindStatusCondition(mcpServer.Status.Conditions, v1alpha1.ReadyConditionType.String())
Expect(ready).NotTo(BeNil())
Expect(ready.Status).To(Equal(metav1.ConditionFalse))
})
})
})
18 changes: 14 additions & 4 deletions go/nexus/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,23 @@ func (in *GenericRouter) RegisterRoutes(r chi.Router) {
}
}

func newRequestContext(r *http.Request) (*schemas.BifrostContext, context.CancelFunc) {
bifrostCtx, cancel := schemas.NewBifrostContextWithCancel(r.Context())
if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyUserAgent, userAgent)
}

// Newer Claude models reject a trailing assistant message, and without a model capability
// catalog bifrost assumes prefill is supported and forwards it. Clients such as Codex can end
// a turn on an assistant message, so have bifrost strip it for Anthropic models.
bifrostCtx.SetValue(schemas.BifrostContextKeySupportsAssistantPrefill, false)
return bifrostCtx, cancel
}

func (in *GenericRouter) createHandler(config RouteConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := config.GetRequestTypeInstance()
bifrostCtx, cancel := schemas.NewBifrostContextWithCancel(r.Context())
if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
bifrostCtx.SetValue(schemas.BifrostContextKeyUserAgent, userAgent)
}
bifrostCtx, cancel := newRequestContext(r)

if config.RequestParser != nil {
if err := config.RequestParser(r, req); err != nil {
Expand Down
Loading
Loading