Skip to content
Merged
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
13 changes: 13 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,19 @@ message AppSpecV3 {
// want the app to be accessible from any of them. If `public_addr` is explicitly set in the app spec,
// setting this value to true will overwrite that public address in the web UI.
bool UseAnyProxyPublicAddr = 14 [(gogoproto.jsontag) = "use_any_proxy_public_addr,omitempty"];
// MCP contains MCP server related configurations.
MCP MCP = 15 [(gogoproto.jsontag) = "mcp,omitempty"];
}

// MCP contains MCP server-related configurations.
message MCP {
// Command to launch stdio-based MCP servers.
string command = 1;
// Args to execute with the command.
repeated string args = 2;
// RunAsHostUser is the host user account under which the command will be
// executed. Required for stdio-based MCP servers.
string run_as_host_user = 3;
}

// Rewrite is a list of rewriting rules to apply to requests and responses.
Expand Down
68 changes: 66 additions & 2 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ type Application interface {
IsGCP() bool
// IsTCP returns true if this app represents a TCP endpoint.
IsTCP() bool
// IsMCP returns true if this app represents a MCP server.
IsMCP() bool
// GetProtocol returns the application protocol.
GetProtocol() string
// GetAWSAccountID returns value of label containing AWS account ID on this app.
Expand Down Expand Up @@ -101,6 +103,8 @@ type Application interface {
SetTCPPorts([]*PortRange)
// GetIdentityCenter fetches identity center info for the app, if any.
GetIdentityCenter() *AppIdentityCenter
// GetMCP fetches MCP specific configuration.
GetMCP() *MCP
}

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

// IsMCP returns true if provided uri is an MCP app.
func (a *AppV3) IsMCP() bool {
return IsAppMCP(a.Spec.URI)
}

func IsAppTCP(uri string) bool {
return strings.HasPrefix(uri, "tcp://")
}

// IsAppMCP returns true if provided uri is an MCP app.
func IsAppMCP(uri string) bool {
return GetMCPServerTransportType(uri) != ""
}

// GetProtocol returns the application protocol.
func (a *AppV3) GetProtocol() string {
if a.IsTCP() {
Expand Down Expand Up @@ -400,10 +414,14 @@ func (a *AppV3) CheckAndSetDefaults() error {
return trace.BadParameter("app %q invalid label key: %q", a.GetName(), key)
}
}

if a.Spec.URI == "" {
if a.Spec.Cloud != "" {
switch {
case a.Spec.Cloud != "":
a.Spec.URI = fmt.Sprintf("cloud://%v", a.Spec.Cloud)
} else {
case a.Spec.MCP != nil && a.Spec.MCP.Command != "":
a.Spec.URI = SchemaMCPStdio
default:
return trace.BadParameter("app %q URI is empty", a.GetName())
}
}
Expand Down Expand Up @@ -453,6 +471,13 @@ func (a *AppV3) CheckAndSetDefaults() error {
}
}

if a.IsMCP() {
a.SetSubKind(SubKindMCP)
if err := a.checkMCP(); err != nil {
return trace.Wrap(err)
}
}

return nil
}

Expand Down Expand Up @@ -486,6 +511,28 @@ func (a *AppV3) checkTCPPorts() error {
return nil
}

func (a *AppV3) checkMCP() error {
switch GetMCPServerTransportType(a.Spec.URI) {
case MCPTransportStdio:
return trace.Wrap(a.checkMCPStdio())
default:
return trace.BadParameter("unsupported MCP server %q with URI %q", a.GetName(), a.Spec.URI)
}
}

func (a *AppV3) checkMCPStdio() error {
if a.Spec.MCP == nil {
return trace.BadParameter("MCP server %q is missing 'mcp' spec", a.GetName())
}
if a.Spec.MCP.Command == "" {
return trace.BadParameter("MCP server %q is missing 'command' which specifies the executable to launch the MCP server. Arguments should be specified through the 'args' field", a.GetName())
}
if a.Spec.MCP.RunAsHostUser == "" {
return trace.BadParameter("MCP server %q is missing 'run_as_host_user' which specifies a valid host user to execute the command", a.GetName())
}
return nil
}

// GetIdentityCenter returns the Identity Center information for the app, if any.
// May be nil.
func (a *AppV3) GetIdentityCenter() *AppIdentityCenter {
Expand All @@ -511,6 +558,11 @@ func (a *AppV3) IsEqual(i Application) bool {
return false
}

// GetMCP returns MCP specific configuration.
func (a *AppV3) GetMCP() *MCP {
return a.Spec.MCP
}

// DeduplicateApps deduplicates apps by combination of app name and public address.
// Apps can have the same name but also could have different addresses.
func DeduplicateApps(apps []Application) (result []Application) {
Expand Down Expand Up @@ -596,3 +648,15 @@ func (p *PortRange) String() string {
return fmt.Sprintf("%d-%d", p.Port, p.EndPort)
}
}

// GetMCPServerTransportType returns the transport of the MCP server based on
// the URI. If no MCP transport type can be determined from the URI, an empty
// string is returned.
func GetMCPServerTransportType(uri string) string {
switch {
case strings.HasPrefix(uri, SchemaMCPStdio):
return MCPTransportStdio
default:
return ""
}
}
78 changes: 78 additions & 0 deletions api/types/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,60 @@ func TestNewAppV3(t *testing.T) {
want: nil,
wantErr: require.Error,
},
{
name: "mcp with command",
meta: Metadata{
Name: "mcp-everything",
},
spec: AppSpecV3{
MCP: &MCP{
Command: "docker",
Args: []string{"run", "-i", "--rm", "mcp/everything"},
RunAsHostUser: "docker",
},
},
want: &AppV3{
Kind: "app",
SubKind: "mcp",
Version: "v3",
Metadata: Metadata{
Name: "mcp-everything",
Namespace: "default",
},
Spec: AppSpecV3{
URI: "mcp+stdio://",
MCP: &MCP{
Command: "docker",
Args: []string{"run", "-i", "--rm", "mcp/everything"},
RunAsHostUser: "docker",
},
},
},
wantErr: require.NoError,
},
{
name: "mcp missing spec",
meta: Metadata{
Name: "mcp-missing-run-as",
},
spec: AppSpecV3{
URI: "mcp+stdio://",
},
wantErr: require.Error,
},
{
name: "mcp missing run_as_host_user",
meta: Metadata{
Name: "mcp-missing-spec",
},
spec: AppSpecV3{
MCP: &MCP{
Command: "docker",
Args: []string{"run", "-i", "--rm", "mcp/everything"},
},
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -658,3 +712,27 @@ func hasErrAndContains(msg string) require.ErrorAssertionFunc {
require.ErrorContains(t, err, msg, msgAndArgs...)
}
}

func TestGetMCPServerTransportType(t *testing.T) {
tests := []struct {
name string
uri string
want string
}{
{
name: "stdio",
uri: "mcp+stdio://",
want: MCPTransportStdio,
},
{
name: "unknown",
uri: "http://localhost",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, GetMCPServerTransportType(tt.uri))
})
}
}
13 changes: 13 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ const (
// KindApp is a web app resource.
KindApp = "app"

// SubKindMCP represents an MCP server as a subkind of app.
SubKindMCP = KindMCP

// KindMCP is an MCP server resource.
// Currently, MCP servers are accessed through apps.
// In the future, they may become a standalone resource kind.
KindMCP = "mcp"

// KindDatabaseServer is a database proxy server resource.
KindDatabaseServer = "db_server"

Expand Down Expand Up @@ -896,6 +904,11 @@ const (
// CloudGCP identifies that a resource was discovered in GCP.
CloudGCP = "GCP"

// SchemaMCPStdio is a URI schema for MCP servers using stdio transport.
SchemaMCPStdio = "mcp+stdio://"
// MCPTransportStdio indicates the MCP server uses stdio transport.
MCPTransportStdio = "stdio"

// DiscoveredResourceNode identifies a discovered SSH node.
DiscoveredResourceNode = "node"
// DiscoveredResourceDatabase identifies a discovered database.
Expand Down
Loading
Loading