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
4 changes: 4 additions & 0 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ func (a *AppV3) checkMCP() error {
}

func (a *AppV3) checkMCPStdio() error {
// Skip validation for internal demo resource.
if resourceType, _ := a.GetLabel(TeleportInternalResourceType); resourceType == DemoResource {
return nil
}
if a.Spec.MCP == nil {
return trace.BadParameter("MCP server %q is missing 'mcp' spec", a.GetName())
}
Expand Down
28 changes: 28 additions & 0 deletions api/types/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,34 @@ func TestNewAppV3(t *testing.T) {
},
wantErr: require.Error,
},
{
name: "mcp demo",
meta: Metadata{
Name: "teleport-mcp-demo",
Labels: map[string]string{
TeleportInternalResourceType: DemoResource,
},
},
spec: AppSpecV3{
URI: "mcp+stdio://teleport-mcp-demo",
},
want: &AppV3{
Kind: "app",
SubKind: "mcp",
Version: "v3",
Metadata: Metadata{
Name: "teleport-mcp-demo",
Namespace: "default",
Labels: map[string]string{
TeleportInternalResourceType: DemoResource,
},
},
Spec: AppSpecV3{
URI: "mcp+stdio://teleport-mcp-demo",
},
},
wantErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
7 changes: 6 additions & 1 deletion api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1155,10 +1155,15 @@ const (
// should not change these resources.
SystemResource = "system"

// PresetResource are resources resources will be created if they don't exist. Updates may be applied
// PresetResource are resources that will be created if they don't exist. Updates may be applied
// to them, but user changes to these resources will be preserved.
PresetResource = "preset"

// DemoResource are resources that demonstrates specific Teleport features.
// These resources are typically managed internally by Teleport and enabled
// via flags. Users should not change these resources.
DemoResource = "demo"

// ProxyGroupIDLabel is the internal-use label for proxy heartbeats that's
// used by reverse tunnel agents to keep track of multiple independent sets
// of proxies in proxy peering mode.
Expand Down
3 changes: 0 additions & 3 deletions integration/appaccess/appaccess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import (
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/srv/app/common"
libmcp "github.com/gravitational/teleport/lib/srv/mcp"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/web/app"
)
Expand All @@ -58,8 +57,6 @@ import (
// It allows to make the entire cluster set up once, instead of per test,
// which speeds things up significantly.
func TestAppAccess(t *testing.T) {
t.Setenv(libmcp.InMemoryServerEnvVar, "true")

pack := Setup(t)

t.Run("Forward", bind(pack, testForward))
Expand Down
6 changes: 3 additions & 3 deletions integration/appaccess/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func testMCP(pack *Pack, t *testing.T) {
testMCPDialStdioNoServerFound(t, pack)
})

t.Run("DialMCPSererver stdio success", func(t *testing.T) {
t.Run("DialMCPServer stdio success", func(t *testing.T) {
testMCPDialStdio(t, pack)
})
}
Expand All @@ -49,7 +49,7 @@ func testMCPDialStdioNoServerFound(t *testing.T, pack *Pack) {
func testMCPDialStdio(t *testing.T, pack *Pack) {
require.NoError(t, pack.tc.SaveProfile(false))

serverConn, err := pack.tc.DialMCPServer(context.Background(), libmcp.InMemoryServerName)
serverConn, err := pack.tc.DialMCPServer(context.Background(), libmcp.DemoServerName)
require.NoError(t, err)

ctx := context.Background()
Expand All @@ -60,5 +60,5 @@ func testMCPDialStdio(t *testing.T, pack *Pack) {

listTools, err := stdioClient.ListTools(ctx, mcp.ListToolsRequest{})
require.NoError(t, err)
require.Len(t, listTools.Tools, 2)
require.Len(t, listTools.Tools, 3)
}
25 changes: 10 additions & 15 deletions integration/appaccess/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -58,8 +58,8 @@ import (
"github.com/gravitational/teleport/lib/srv/alpnproxy"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/srv/app/common"
libmcp "github.com/gravitational/teleport/lib/srv/mcp"
"github.com/gravitational/teleport/lib/utils"
sliceutils "github.com/gravitational/teleport/lib/utils/slices"
"github.com/gravitational/teleport/lib/web"
"github.com/gravitational/teleport/lib/web/app"
websession "github.com/gravitational/teleport/lib/web/session"
Expand Down Expand Up @@ -772,6 +772,7 @@ func (p *Pack) startRootAppServers(t *testing.T, count int, opts AppTestOptions)
raConf.Proxy.Enabled = false
raConf.SSH.Enabled = false
raConf.Apps.Enabled = true
raConf.Apps.MCPDemoServer = true
raConf.CircuitBreakerConfig = breaker.NoopBreakerConfig()
raConf.Apps.MonitorCloseChannel = opts.MonitorCloseChannel
raConf.Apps.Apps = append([]servicecfg.App{
Expand Down Expand Up @@ -1064,12 +1065,6 @@ func (p *Pack) startLeafAppServers(t *testing.T, count int, opts AppTestOptions)
}

func waitForAppRegInRemoteSiteCache(t *testing.T, tunnel reversetunnelclient.Server, clusterName string, cfgApps []servicecfg.App, hostUUID string) {
if os.Getenv(libmcp.InMemoryServerEnvVar) == "true" {
cfgApps = append(cfgApps, servicecfg.App{
Name: libmcp.InMemoryServerName,
})
}

require.EventuallyWithT(t, func(t *assert.CollectT) {
site, err := tunnel.GetSite(clusterName)
assert.NoError(t, err)
Expand All @@ -1080,12 +1075,12 @@ func waitForAppRegInRemoteSiteCache(t *testing.T, tunnel reversetunnelclient.Ser
apps, err := ap.GetApplicationServers(context.Background(), apidefaults.Namespace)
assert.NoError(t, err)

counter := 0
for _, v := range apps {
if v.GetHostID() == hostUUID {
counter++
}
}
assert.Len(t, cfgApps, counter)
wantNames := sliceutils.Map(cfgApps, func(app servicecfg.App) string {
return app.Name
})
assert.Subset(t,
slices.Collect(types.ResourceNames(apps)),
wantNames,
)
}, time.Minute*2, time.Millisecond*200)
}
19 changes: 16 additions & 3 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ type CommandLineFlags struct {
// AppPublicAddr is the public address of the application to proxy.
AppPublicAddr string

// MCPDemoServer enables the "Teleport Demo" MCP server.
MCPDemoServer bool

// DatabaseName is the name of the database to proxy.
DatabaseName string
// DatabaseDescription is a free-form database description.
Expand Down Expand Up @@ -1924,6 +1927,9 @@ func applyAppsConfig(fc *FileConfig, cfg *servicecfg.Config) error {
// Enable debugging application if requested.
cfg.Apps.DebugApp = fc.Apps.DebugApp

// Enable the "Teleport Demo" MCP server if requested.
cfg.Apps.MCPDemoServer = fc.Apps.MCPDemoServer

// Configure resource watcher selectors if present.
for _, matcher := range fc.Apps.ResourceMatchers {
if matcher.AWS.AssumeRoleARN != "" {
Expand Down Expand Up @@ -2400,29 +2406,36 @@ func Configure(clf *CommandLineFlags, cfg *servicecfg.Config, legacyAppFlags boo
// If this process is trying to join a cluster as an application service,
// make sure application name and URI are provided.
if slices.Contains(splitRoles(clf.Roles), defaults.RoleApp) {
if (clf.AppName == "") && (clf.AppURI == "" && clf.AppCloud == "") {
if (clf.AppName == "") && (clf.AppURI == "" && clf.AppCloud == "" && !clf.MCPDemoServer) {
// TODO: remove legacyAppFlags once `teleport start --app-name` is removed.
if legacyAppFlags {
return trace.BadParameter("application name (--app-name) and URI (--app-uri) flags are both required to join application proxy to the cluster")
}
return trace.BadParameter("to join application proxy to the cluster provide application name (--name) and either URI (--uri) or Cloud type (--cloud)")
}

if clf.AppName == "" {
if clf.AppName == "" && !clf.MCPDemoServer {
if legacyAppFlags {
return trace.BadParameter("application name (--app-name) is required to join application proxy to the cluster")
}
return trace.BadParameter("to join application proxy to the cluster provide application name (--name)")
}

if clf.AppURI == "" && clf.AppCloud == "" {
if clf.AppName != "" && clf.AppURI == "" && clf.AppCloud == "" {
if legacyAppFlags {
return trace.BadParameter("URI (--app-uri) flag is required to join application proxy to the cluster")
}
return trace.BadParameter("to join application proxy to the cluster provide URI (--uri) or Cloud type (--cloud)")
}
}

// Enable the "Teleport Demo" MCP server if requested. Make sure application
// service is enabled for proxying MCP servers.
if clf.MCPDemoServer {
Comment thread
greedy52 marked this conversation as resolved.
cfg.Apps.MCPDemoServer = true
cfg.Apps.Enabled = true
}

// If application name was specified on command line, add to file
// configuration where it will be validated.
if clf.AppName != "" {
Expand Down
23 changes: 19 additions & 4 deletions lib/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2572,7 +2572,10 @@ func TestAppsCLF(t *testing.T) {
inAppURI string
inAppCloud string
inLegacyAppFlags bool
inMCPDemoServer bool

outApps []servicecfg.App
outMCPDemoServer bool
requireError require.ErrorAssertionFunc
}{
{
Expand Down Expand Up @@ -2707,20 +2710,32 @@ func TestAppsCLF(t *testing.T) {
require.ErrorContains(t, err, "missing application \"foo\" URI")
},
},
{
desc: "mcp demo server",
inRoles: defaults.RoleApp,
inAppName: "",
inAppURI: "",
inMCPDemoServer: true,
outApps: nil,
outMCPDemoServer: true,
requireError: require.NoError,
},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
clf := CommandLineFlags{
Roles: tt.inRoles,
AppName: tt.inAppName,
AppURI: tt.inAppURI,
AppCloud: tt.inAppCloud,
Roles: tt.inRoles,
AppName: tt.inAppName,
AppURI: tt.inAppURI,
AppCloud: tt.inAppCloud,
MCPDemoServer: tt.inMCPDemoServer,
}
cfg := servicecfg.MakeDefaultConfig()
err := Configure(&clf, cfg, tt.inLegacyAppFlags)
tt.requireError(t, err)
require.Equal(t, tt.outApps, cfg.Apps.Apps)
require.Equal(t, tt.outMCPDemoServer, cfg.Apps.MCPDemoServer)
})
}
}
Expand Down
32 changes: 22 additions & 10 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ type SampleFlags struct {
AppName string
// AppURI is the internal address of the application to proxy
AppURI string
// MCPDemoServer enables the "Teleport Demo" MCP server.
MCPDemoServer bool
// NodeLabels is list of labels in the format `foo=bar,baz=bax` to add to newly created nodes.
NodeLabels string
// CAPin is the SKPI hash of the CA used to verify the Auth Server. Can be
Expand Down Expand Up @@ -410,17 +412,24 @@ func makeSampleProxyConfig(conf *servicecfg.Config, flags SampleFlags, enabled b
func makeSampleAppsConfig(conf *servicecfg.Config, flags SampleFlags, enabled bool) (Apps, error) {
var apps Apps
// assume users want app role if they added app name and/or uri but didn't add app role
if enabled || flags.AppURI != "" || flags.AppName != "" {
if flags.AppURI == "" || flags.AppName == "" {
return Apps{}, trace.BadParameter("please provide both --app-name and --app-uri")
}

if enabled || flags.AppURI != "" || flags.AppName != "" || flags.MCPDemoServer {
apps.EnabledFlag = "yes"
apps.Apps = []*App{
{
Name: flags.AppName,
URI: flags.AppURI,
},
apps.MCPDemoServer = flags.MCPDemoServer

switch {
case flags.AppURI != "" && flags.AppName != "":
apps.Apps = []*App{
{
Name: flags.AppName,
URI: flags.AppURI,
},
}

case flags.MCPDemoServer && flags.AppURI == "" && flags.AppName == "":
// This is ok if only MCPDemoServer is set.

default:
return Apps{}, trace.BadParameter("please provide both --app-name and --app-uri")
}
}

Expand Down Expand Up @@ -2083,6 +2092,9 @@ type Apps struct {
// DebugApp turns on a header debugging application.
DebugApp bool `yaml:"debug_app"`

// MCPDemoServer enables the "Teleport Demo" MCP server.
MCPDemoServer bool `yaml:"mcp_demo_server"`

// Apps is a list of applications that will be run by this service.
Apps []*App `yaml:"apps"`

Expand Down
43 changes: 43 additions & 0 deletions lib/config/fileconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,49 @@ func TestMakeSampleFileConfig(t *testing.T) {
require.Equal(t, "yes", fc.Apps.EnabledFlag)
})

t.Run("App role with MCP Demo server", func(t *testing.T) {
fc, err := MakeSampleFileConfig(SampleFlags{
Roles: "app",
MCPDemoServer: true,
})
require.NoError(t, err)
require.Equal(t, "no", fc.SSH.EnabledFlag)
require.Equal(t, "no", fc.Proxy.EnabledFlag)
require.Equal(t, "no", fc.Auth.EnabledFlag)
require.Equal(t, "yes", fc.Apps.EnabledFlag)
require.True(t, fc.Apps.MCPDemoServer)
})

t.Run("App name and MCP Demo Server", func(t *testing.T) {
_, err := MakeSampleFileConfig(SampleFlags{
Roles: "app",
AppURI: "localhost:8080",
MCPDemoServer: true,
})
require.Error(t, err)

_, err = MakeSampleFileConfig(SampleFlags{
Roles: "app",
AppName: "nginx",
MCPDemoServer: true,
})
require.Error(t, err)

fc, err := MakeSampleFileConfig(SampleFlags{
Roles: "app",
AppURI: "localhost:8080",
AppName: "nginx",
MCPDemoServer: true,
})
require.NoError(t, err)

require.Equal(t, "no", fc.SSH.EnabledFlag)
require.Equal(t, "no", fc.Proxy.EnabledFlag)
require.Equal(t, "no", fc.Auth.EnabledFlag)
require.Equal(t, "yes", fc.Apps.EnabledFlag)
require.True(t, fc.Apps.MCPDemoServer)
})

t.Run("Proxy role", func(t *testing.T) {
fc, err := MakeSampleFileConfig(SampleFlags{
Roles: "proxy",
Expand Down
Loading
Loading