@@ -3,6 +3,7 @@ package pomerium
33import (
44 "encoding/base64"
55 "fmt"
6+ "strconv"
67 "strings"
78
89 envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
7273 model .KubernetesServiceAccountTokenSecret ,
7374 model .SetRequestHeadersSecret ,
7475 model .SetResponseHeadersSecret ,
76+ model .MCPServerUpstreamOAuth2Secret ,
77+ })
78+ mcpServerAnnotations = boolMap ([]string {
79+ model .MCPServer ,
80+ model .MCPServerMaxRequestBytes ,
81+ model .MCPServerUpstreamOAuth2TokenURL ,
82+ model .MCPServerUpstreamOAuth2Scopes ,
83+ })
84+ mcpClientAnnotations = boolMap ([]string {
85+ model .MCPClient ,
7586 })
7687 handledElsewhere = boolMap ([]string {
7788 model .PathRegex ,
@@ -95,18 +106,20 @@ func boolMap(keys []string) map[string]bool {
95106}
96107
97108type keys struct {
98- Base , Envoy , Policy , TLS , Etc , Secret map [string ]string
109+ Base , Envoy , Policy , TLS , Etc , Secret , MCPServer , MCPClient map [string ]string
99110}
100111
101112func removeKeyPrefix (src map [string ]string , prefix string ) (* keys , error ) {
102113 prefix = fmt .Sprintf ("%s/" , prefix )
103114 kv := keys {
104- Base : make (map [string ]string ),
105- Envoy : make (map [string ]string ),
106- Policy : make (map [string ]string ),
107- TLS : make (map [string ]string ),
108- Etc : make (map [string ]string ),
109- Secret : make (map [string ]string ),
115+ Base : make (map [string ]string ),
116+ Envoy : make (map [string ]string ),
117+ Policy : make (map [string ]string ),
118+ TLS : make (map [string ]string ),
119+ Etc : make (map [string ]string ),
120+ Secret : make (map [string ]string ),
121+ MCPServer : make (map [string ]string ),
122+ MCPClient : make (map [string ]string ),
110123 }
111124
112125 for k , v := range src {
@@ -129,6 +142,8 @@ func removeKeyPrefix(src map[string]string, prefix string) (*keys, error) {
129142 {policyAnnotations , kv .Policy },
130143 {tlsAnnotations , kv .TLS },
131144 {secretAnnotations , kv .Secret },
145+ {mcpServerAnnotations , kv .MCPServer },
146+ {mcpClientAnnotations , kv .MCPClient },
132147 {handledElsewhere , kv .Etc },
133148 } {
134149 if m .keys [k ] {
@@ -167,6 +182,9 @@ func applyAnnotations(
167182 if err = applySecretAnnotations (r , kv .Secret , ic .Secrets , ic .Ingress .Namespace ); err != nil {
168183 return err
169184 }
185+ if err = applyMCPAnnotations (r , kv .MCPServer , kv .MCPClient ); err != nil {
186+ return err
187+ }
170188 p := new (pomerium.Policy )
171189 r .Policies = []* pomerium.Policy {p }
172190 if err := unmarshalPolicyAnnotations (p , kv .Policy ); err != nil {
@@ -285,6 +303,47 @@ func applySecretAnnotations(
285303 return nil
286304 },
287305 },
306+ model .MCPServerUpstreamOAuth2Secret : {
307+ corev1 .SecretTypeOpaque ,
308+ func (data map [string ][]byte ) error {
309+ clientID , hasClientID := data [model .MCPServerUpstreamOAuth2ClientIDKey ]
310+ clientSecret , hasClientSecret := data [model .MCPServerUpstreamOAuth2ClientSecretKey ]
311+
312+ if ! hasClientID && ! hasClientSecret {
313+ return fmt .Errorf ("secret must have at least one of %s or %s keys" ,
314+ model .MCPServerUpstreamOAuth2ClientIDKey , model .MCPServerUpstreamOAuth2ClientSecretKey )
315+ }
316+
317+ if r .Mcp == nil {
318+ r .Mcp = & pomerium.MCP {
319+ Mode : & pomerium.MCP_Server {
320+ Server : & pomerium.MCPServer {},
321+ },
322+ }
323+ } else if r .Mcp .GetServer () == nil {
324+ if r .Mcp .GetClient () != nil {
325+ return fmt .Errorf ("route is already configured as MCP client, cannot add OAuth2 secret" )
326+ }
327+ r .Mcp .Mode = & pomerium.MCP_Server {
328+ Server : & pomerium.MCPServer {},
329+ }
330+ }
331+
332+ server := r .Mcp .GetServer ()
333+ if server .UpstreamOauth2 == nil {
334+ server .UpstreamOauth2 = & pomerium.UpstreamOAuth2 {}
335+ }
336+
337+ if hasClientID {
338+ server .UpstreamOauth2 .ClientId = string (clientID )
339+ }
340+ if hasClientSecret {
341+ server .UpstreamOauth2 .ClientSecret = string (clientSecret )
342+ }
343+
344+ return nil
345+ },
346+ },
288347 }
289348
290349 for key , secretName := range annotations {
@@ -310,6 +369,113 @@ func applySecretAnnotations(
310369 return nil
311370}
312371
372+ func applyMCPAnnotations (r * pomerium.Route , serverKVs , clientKVs map [string ]string ) error {
373+ hasServer := len (serverKVs ) > 0
374+ hasClient := len (clientKVs ) > 0
375+
376+ if hasServer && hasClient {
377+ return fmt .Errorf ("cannot specify both MCP server and client configurations" )
378+ }
379+
380+ if hasServer {
381+ return applyMCPServerAnnotations (r , serverKVs )
382+ }
383+
384+ if hasClient {
385+ return applyMCPClientAnnotations (r , clientKVs )
386+ }
387+
388+ r .Mcp = nil
389+ return nil
390+ }
391+
392+ func applyMCPServerAnnotations (r * pomerium.Route , kvs map [string ]string ) error {
393+ // Initialize or get existing server config
394+ var serverConfig * pomerium.MCPServer
395+ if r .Mcp != nil && r .Mcp .GetServer () != nil {
396+ serverConfig = r .Mcp .GetServer ()
397+ } else {
398+ serverConfig = & pomerium.MCPServer {}
399+ }
400+
401+ for k , v := range kvs {
402+ if v == "" && k != model .MCPServer {
403+ continue
404+ }
405+ switch k {
406+ case model .MCPServer :
407+ if v != "" && v != "true" {
408+ return fmt .Errorf ("mcp_server annotation should be 'true' or omitted, got %q" , v )
409+ }
410+ continue
411+ case model .MCPServerMaxRequestBytes :
412+ val , err := strconv .ParseUint (v , 10 , 32 )
413+ if err != nil {
414+ return fmt .Errorf ("invalid max_request_bytes value %q: %w" , v , err )
415+ }
416+ maxBytes := uint32 (val )
417+ serverConfig .MaxRequestBytes = & maxBytes
418+ case model .MCPServerUpstreamOAuth2TokenURL :
419+ if serverConfig .UpstreamOauth2 == nil {
420+ serverConfig .UpstreamOauth2 = & pomerium.UpstreamOAuth2 {}
421+ }
422+ if serverConfig .UpstreamOauth2 .Oauth2Endpoint == nil {
423+ serverConfig .UpstreamOauth2 .Oauth2Endpoint = & pomerium.OAuth2Endpoint {}
424+ }
425+ serverConfig .UpstreamOauth2 .Oauth2Endpoint .TokenUrl = v
426+ case model .MCPServerUpstreamOAuth2Scopes :
427+ if serverConfig .UpstreamOauth2 == nil {
428+ serverConfig .UpstreamOauth2 = & pomerium.UpstreamOAuth2 {}
429+ }
430+ serverConfig .UpstreamOauth2 .Scopes = strings .Split (v , "," )
431+ for i , scope := range serverConfig .UpstreamOauth2 .Scopes {
432+ serverConfig .UpstreamOauth2 .Scopes [i ] = strings .TrimSpace (scope )
433+ }
434+ default :
435+ return fmt .Errorf ("unknown MCP server annotation %s" , k )
436+ }
437+ }
438+
439+ // Only create new MCP structure if it doesn't exist
440+ if r .Mcp == nil {
441+ r .Mcp = & pomerium.MCP {
442+ Mode : & pomerium.MCP_Server {
443+ Server : serverConfig ,
444+ },
445+ }
446+ } else if r .Mcp .GetServer () == nil {
447+ if r .Mcp .GetClient () != nil {
448+ return fmt .Errorf ("route is already configured as MCP client, cannot add server configuration" )
449+ }
450+ r .Mcp .Mode = & pomerium.MCP_Server {
451+ Server : serverConfig ,
452+ }
453+ }
454+ return nil
455+ }
456+
457+ func applyMCPClientAnnotations (r * pomerium.Route , kvs map [string ]string ) error {
458+ for k , v := range kvs {
459+ switch k {
460+ case model .MCPClient :
461+ if v != "" && v != "true" {
462+ return fmt .Errorf ("mcp_client annotation should be 'true' or omitted, got %q" , v )
463+ }
464+ continue
465+ default :
466+ return fmt .Errorf ("unknown MCP client annotation %s" , k )
467+ }
468+ }
469+
470+ clientConfig := & pomerium.MCPClient {}
471+ r .Mcp = & pomerium.MCP {
472+ Mode : & pomerium.MCP_Client {
473+ Client : clientConfig ,
474+ },
475+ }
476+ return nil
477+ }
478+
313479func b64 (secret * corev1.Secret , annotation , key string ) (string , error ) {
314480 data := secret .Data [key ]
315481 if len (data ) == 0 {
0 commit comments