Skip to content

Commit 79cd1e5

Browse files
committed
MCP access part 1: update app definition and config
1 parent 07bba43 commit 79cd1e5

File tree

14 files changed

+3622
-2724
lines changed

14 files changed

+3622
-2724
lines changed

api/proto/teleport/legacy/types/types.proto

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,24 @@ message AppSpecV3 {
11181118
// want the app to be accessible from any of them. If `public_addr` is explicitly set in the app spec,
11191119
// setting this value to true will overwrite that public address in the web UI.
11201120
bool UseAnyProxyPublicAddr = 14 [(gogoproto.jsontag) = "use_any_proxy_public_addr,omitempty"];
1121+
// MCP contains MCP server related configurations.
1122+
MCP MCP = 15 [(gogoproto.jsontag) = "mcp,omitempty"];
1123+
}
1124+
1125+
// MCP contains MCP server-related configurations.
1126+
message MCP {
1127+
// Command to launch stdio-based MCP servers.
1128+
string command = 1;
1129+
// Args to execute with the command.
1130+
repeated string args = 2;
1131+
// RunAsLocalUser is the local user account under which the command will be
1132+
// executed. Required for stdio-based MCP servers.
1133+
string run_as_local_user = 3;
1134+
// StopSignal specifies the OS signal to send for gracefully stopping the
1135+
// process. If not set, defaults to 0x2 (SIGINT) as it is a common signal for
1136+
// stopping programs listening on stdin. Signal 0x9 (SIGKILL) is sent
1137+
// automatically after 10 seconds if the process has not exited.
1138+
uint32 stop_signal = 4;
11211139
}
11221140

11231141
// Rewrite is a list of rewriting rules to apply to requests and responses.

api/types/app.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ type Application interface {
7070
IsGCP() bool
7171
// IsTCP returns true if this app represents a TCP endpoint.
7272
IsTCP() bool
73+
// IsMCP returns true if this app represents a MCP server.
74+
IsMCP() bool
7375
// GetProtocol returns the application protocol.
7476
GetProtocol() string
7577
// GetAWSAccountID returns value of label containing AWS account ID on this app.
@@ -101,6 +103,8 @@ type Application interface {
101103
SetTCPPorts([]*PortRange)
102104
// GetIdentityCenter fetches identity center info for the app, if any.
103105
GetIdentityCenter() *AppIdentityCenter
106+
// GetMCP fetches MCP specific configuration.
107+
GetMCP() *MCP
104108
}
105109

106110
// NewAppV3 creates a new app resource.
@@ -286,10 +290,20 @@ func (a *AppV3) IsTCP() bool {
286290
return IsAppTCP(a.Spec.URI)
287291
}
288292

293+
// IsMCP returns true if this app represents a TCP endpoint.
294+
func (a *AppV3) IsMCP() bool {
295+
return IsAppMCP(a.Spec.URI)
296+
}
297+
289298
func IsAppTCP(uri string) bool {
290299
return strings.HasPrefix(uri, "tcp://")
291300
}
292301

302+
// IsAppMCP returns true if provided uri is an MCP app.
303+
func IsAppMCP(uri string) bool {
304+
return GetMCPServerTransportType(uri) != ""
305+
}
306+
293307
// GetProtocol returns the application protocol.
294308
func (a *AppV3) GetProtocol() string {
295309
if a.IsTCP() {
@@ -400,6 +414,11 @@ func (a *AppV3) CheckAndSetDefaults() error {
400414
return trace.BadParameter("app %q invalid label key: %q", a.GetName(), key)
401415
}
402416
}
417+
418+
if a.Spec.MCP != nil && a.Spec.MCP.Command != "" {
419+
a.Spec.URI = SchemaMCPStdio
420+
}
421+
403422
if a.Spec.URI == "" {
404423
if a.Spec.Cloud != "" {
405424
a.Spec.URI = fmt.Sprintf("cloud://%v", a.Spec.Cloud)
@@ -453,6 +472,13 @@ func (a *AppV3) CheckAndSetDefaults() error {
453472
}
454473
}
455474

475+
if a.IsMCP() {
476+
a.SetSubKind(SubKindMCP)
477+
if err := a.checkMCP(); err != nil {
478+
return trace.Wrap(err)
479+
}
480+
}
481+
456482
return nil
457483
}
458484

@@ -486,6 +512,28 @@ func (a *AppV3) checkTCPPorts() error {
486512
return nil
487513
}
488514

515+
func (a *AppV3) checkMCP() error {
516+
switch {
517+
case strings.HasPrefix(a.Spec.URI, SchemaMCPStdio):
518+
return trace.Wrap(a.checkMCPStdio())
519+
default:
520+
return trace.BadParameter("app %q has unsupported MCP URI format", a.GetName())
521+
}
522+
}
523+
524+
func (a *AppV3) checkMCPStdio() error {
525+
if a.Spec.MCP == nil {
526+
return trace.BadParameter("MCP server %q missing mcp spec", a.GetName())
527+
}
528+
if a.Spec.MCP.Command == "" {
529+
return trace.BadParameter("MCP server %q missing command", a.GetName())
530+
}
531+
if a.Spec.MCP.RunAsLocalUser == "" {
532+
return trace.BadParameter("MCP server %q missing run_as_local_user", a.GetName())
533+
}
534+
return nil
535+
}
536+
489537
// GetIdentityCenter returns the Identity Center information for the app, if any.
490538
// May be nil.
491539
func (a *AppV3) GetIdentityCenter() *AppIdentityCenter {
@@ -511,6 +559,11 @@ func (a *AppV3) IsEqual(i Application) bool {
511559
return false
512560
}
513561

562+
// GetMCP returns MCP specific configuration.
563+
func (a *AppV3) GetMCP() *MCP {
564+
return a.Spec.MCP
565+
}
566+
514567
// DeduplicateApps deduplicates apps by combination of app name and public address.
515568
// Apps can have the same name but also could have different addresses.
516569
func DeduplicateApps(apps []Application) (result []Application) {
@@ -596,3 +649,15 @@ func (p *PortRange) String() string {
596649
return fmt.Sprintf("%d-%d", p.Port, p.EndPort)
597650
}
598651
}
652+
653+
// GetMCPServerTransportType returns the transport of the MCP server based on
654+
// the URI. If no MCP transport type can be deferred from the URI, an empty
655+
// string is returned.
656+
func GetMCPServerTransportType(uri string) string {
657+
switch {
658+
case strings.HasPrefix(uri, SchemaMCPStdio):
659+
return MCPTransportStdio
660+
default:
661+
return ""
662+
}
663+
}

api/types/app_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,60 @@ func TestNewAppV3(t *testing.T) {
603603
want: nil,
604604
wantErr: require.Error,
605605
},
606+
{
607+
name: "mcp with command",
608+
meta: Metadata{
609+
Name: "mcp-everything",
610+
},
611+
spec: AppSpecV3{
612+
MCP: &MCP{
613+
Command: "docker",
614+
Args: []string{"run", "-i", "--rm", "mcp/everything"},
615+
RunAsLocalUser: "docker",
616+
},
617+
},
618+
want: &AppV3{
619+
Kind: "app",
620+
SubKind: "mcp",
621+
Version: "v3",
622+
Metadata: Metadata{
623+
Name: "mcp-everything",
624+
Namespace: "default",
625+
},
626+
Spec: AppSpecV3{
627+
URI: "mcp+stdio://",
628+
MCP: &MCP{
629+
Command: "docker",
630+
Args: []string{"run", "-i", "--rm", "mcp/everything"},
631+
RunAsLocalUser: "docker",
632+
},
633+
},
634+
},
635+
wantErr: require.NoError,
636+
},
637+
{
638+
name: "mcp missing spec",
639+
meta: Metadata{
640+
Name: "mcp-missing-run-as",
641+
},
642+
spec: AppSpecV3{
643+
URI: "mcp+stdio://",
644+
},
645+
wantErr: require.Error,
646+
},
647+
{
648+
name: "mcp missing run_as_local_user",
649+
meta: Metadata{
650+
Name: "mcp-missing-spec",
651+
},
652+
spec: AppSpecV3{
653+
MCP: &MCP{
654+
Command: "docker",
655+
Args: []string{"run", "-i", "--rm", "mcp/everything"},
656+
},
657+
},
658+
wantErr: require.Error,
659+
},
606660
}
607661
for _, tt := range tests {
608662
t.Run(tt.name, func(t *testing.T) {
@@ -658,3 +712,27 @@ func hasErrAndContains(msg string) require.ErrorAssertionFunc {
658712
require.ErrorContains(t, err, msg, msgAndArgs...)
659713
}
660714
}
715+
716+
func TestGetMCPServerTransportType(t *testing.T) {
717+
tests := []struct {
718+
name string
719+
uri string
720+
want string
721+
}{
722+
{
723+
name: "stdio",
724+
uri: "mcp+stdio://",
725+
want: MCPTransportStdio,
726+
},
727+
{
728+
name: "unknown",
729+
uri: "http://localhost",
730+
want: "",
731+
},
732+
}
733+
for _, tt := range tests {
734+
t.Run(tt.name, func(t *testing.T) {
735+
require.Equal(t, tt.want, GetMCPServerTransportType(tt.uri))
736+
})
737+
}
738+
}

api/types/constants.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,14 @@ const (
183183
// KindApp is a web app resource.
184184
KindApp = "app"
185185

186+
// SubKindMCP represents an MCP server as a subkind of app.
187+
SubKindMCP = KindMCP
188+
189+
// KindMCP is an MCP server resource.
190+
// Currently, MCP servers are accessed through apps.
191+
// In the future, they may become a standalone resource kind.
192+
KindMCP = "mcp"
193+
186194
// KindDatabaseServer is a database proxy server resource.
187195
KindDatabaseServer = "db_server"
188196

@@ -889,6 +897,11 @@ const (
889897
// CloudGCP identifies that a resource was discovered in GCP.
890898
CloudGCP = "GCP"
891899

900+
// SchemaMCPStdio is a URI schema for MCP servers using stdio transport.
901+
SchemaMCPStdio = "mcp+stdio://"
902+
// MCPTransportStdio indicates the MCP server uses stdio transport.
903+
MCPTransportStdio = "stdio"
904+
892905
// DiscoveredResourceNode identifies a discovered SSH node.
893906
DiscoveredResourceNode = "node"
894907
// DiscoveredResourceDatabase identifies a discovered database.

0 commit comments

Comments
 (0)