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
2 changes: 1 addition & 1 deletion tool/tsh/common/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func onAppLogin(cf *CLIConf) error {
defer clusterClient.Close()

if app.IsMCP() {
return trace.BadParameter("MCP applications are not supported. Please see 'tsh mcp login --help' for more details.")
return trace.BadParameter("MCP applications are not supported. Please see 'tsh mcp config --help' for more details.")
}

if err := validateTargetPort(app, int(cf.TargetPort)); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion tool/tsh/common/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ func onProxyCommandApp(cf *CLIConf) error {
}

if app.IsMCP() {
return trace.BadParameter("MCP applications are not supported. Please see 'tsh mcp login --help' for more details.")
return trace.BadParameter("MCP applications are not supported. Please see 'tsh mcp config --help' for more details.")
}

proxyApp, err := newLocalProxyAppWithPortMapping(cf.Context, tc, profile, appInfo.RouteToApp, app, portMapping, cf.InsecureSkipVerify)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/packages/design/src/ResourceIcon/assets/mcpvscode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions web/packages/design/src/ResourceIcon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ import mattermostDark from './assets/mattermost-dark.svg?no-inline';
import mattermostLight from './assets/mattermost-light.svg?no-inline';
import maxioDark from './assets/maxio-dark.svg?no-inline';
import maxioLight from './assets/maxio-light.svg?no-inline';
import mcpCursorDark from './assets/mcpcursor-dark.svg';
import mcpCursorLight from './assets/mcpcursor-dark.svg';
import mcpVscode from './assets/mcpvscode.svg';
import mcpVscodeInsiders from './assets/mcpVscodeinsiders.svg';
import metabase from './assets/metabase.svg?no-inline';
import microsoft from './assets/microsoft.svg?no-inline';
import microsoftexcel from './assets/microsoftexcel.svg?no-inline';
Expand Down Expand Up @@ -480,6 +484,10 @@ export {
mattermostLight,
maxioDark,
maxioLight,
mcpCursorDark,
mcpCursorLight,
mcpVscode,
mcpVscodeInsiders,
metabase,
microsoft,
microsoftexcel,
Expand Down
3 changes: 3 additions & 0 deletions web/packages/design/src/ResourceIcon/resourceIconSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export const resourceIconSpecs = {
mariadb: { dark: i.mariadbDark, light: i.mariadbLight },
mattermost: { dark: i.mattermostDark, light: i.mattermostLight },
maxio: { dark: i.maxioDark, light: i.maxioLight },
mcpCursor: { dark: i.mcpCursorDark, light: i.mcpCursorLight },
mcpVscode: forAllThemes(i.mcpVscode),
mcpVscodeInsiders: forAllThemes(i.mcpVscodeInsiders),
metabase: forAllThemes(i.metabase),
microsoft: forAllThemes(i.microsoft),
microsoftexcel: forAllThemes(i.microsoftexcel),
Expand Down
55 changes: 55 additions & 0 deletions web/packages/shared/services/mcp/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import {
generateClaudeDesktopConfigForApp,
generateInstallLinksForApp,
} from 'shared/services/mcp/client';

describe('generateClaudeDesktopConfigForApp', () => {
it('generates the correct config JSON', () => {
const outputJSON = generateClaudeDesktopConfigForApp('demo-test');

expect(outputJSON).toBe(`{
"mcpServers": {
"teleport-mcp-demo-test": {
"command": "tsh",
"args": [
"mcp",
"connect",
"demo-test"
]
}
}
}`);
});
});

describe('generateInstallLinksForApp', () => {
it('generates the correct links', () => {
const links = generateInstallLinksForApp('demo-test');
expect(links).toEqual({
cursor:
'cursor://anysphere.cursor-deeplink/mcp/install?name=teleport-mcp-demo-test&config=eyJjb21tYW5kIjoidHNoIiwiYXJncyI6WyJtY3AiLCJjb25uZWN0IiwiZGVtby10ZXN0Il19',
vscode:
'vscode:mcp/install?%7B%22name%22%3A%22teleport-mcp-demo-test%22%2C%22command%22%3A%22tsh%22%2C%22args%22%3A%5B%22mcp%22%2C%22connect%22%2C%22demo-test%22%5D%7D',
vscodeInsiders:
'vscode-insiders:mcp/install?%7B%22name%22%3A%22teleport-mcp-demo-test%22%2C%22command%22%3A%22tsh%22%2C%22args%22%3A%5B%22mcp%22%2C%22connect%22%2C%22demo-test%22%5D%7D',
});
});
});
83 changes: 83 additions & 0 deletions web/packages/shared/services/mcp/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export type MCPServerConfig = {
command: string;
args: string[];
};

function mcpServerNameForApp(appName: string): string {
return `teleport-mcp-${appName}`;
}

function mcpServerConfigForApp(appName: string): MCPServerConfig {
return {
// TODO(greedy52) we might need different command path and TELEPORT_HOME
// env var for Teleport Connect.
command: 'tsh',
args: ['mcp', 'connect', appName],
};
}

/**
* generateClaudeDesktopConfigForApp generates a prettified JSON config with
* details to launch the MCP server app with tsh in Claude Desktop format.
*/
export function generateClaudeDesktopConfigForApp(appName: string): string {
const claudeConfig = {
mcpServers: {
[mcpServerNameForApp(appName)]: mcpServerConfigForApp(appName),
},
};
return JSON.stringify(claudeConfig, null, 2);
}

export type InstallLinks = {
cursor: string;
vscode: string;
vscodeInsiders: string;
};

/**
* generateInstallLinksForApp generates links that can be used to install the MCP
* server app (that runs via tsh) for various MCP clients like cursor.
*/
export function generateInstallLinksForApp(appName: string): InstallLinks {
const name = mcpServerNameForApp(appName);
const config = mcpServerConfigForApp(appName);

// Cursor Deeplink
// https://docs.cursor.com/tools/developers
const cursorLink = new URL('cursor://anysphere.cursor-deeplink/mcp/install');
cursorLink.searchParams.set('name', name);
cursorLink.searchParams.set('config', btoa(JSON.stringify(config)));

// VSCode
// https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_url-handler
const vscodeEncodedConfig = encodeURIComponent(
JSON.stringify({
name,
...config,
})
);
return {
cursor: cursorLink.toString(),
vscode: `vscode:mcp/install?${vscodeEncodedConfig}`,
vscodeInsiders: `vscode-insiders:mcp/install?${vscodeEncodedConfig}`,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding a possible Connect implementation: This will be a bit of a pain to move to Connect. All external links are blocked by default, with a few exceptions, like hosts of currently added clusters, goteleport.com so that we can link to the docs.

The general principle we try to follow is that even if an attacker manages to get XSS in Connect, we want to limit how they might exploit it. In this case it means allowing access only to a trusted set of hosts.

With the deep links to Cursor and VSCode, I suppose we'd need to grant blanket access to mcp/install with any config because I don't see how we could restrict it in a more granular fashion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the install links has special schemas like cursor://, vscode:// and they open those desktop apps, a little different from https://. maybe we need to re-evaluate the security risk for those. well, once the desktop app opens the install link, usually it's a dialog confirm on the MCP server config to be added.

so i think for Connect maybe we can whitelist the install links, with the schema, but without the parameters. if we need to do extra validations, we can extract details from the parameters like validating correct command is used.

};
}
19 changes: 19 additions & 0 deletions web/packages/shared/services/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export * from './client';
60 changes: 52 additions & 8 deletions web/packages/teleport/src/Apps/MCPAppConnectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Box, ButtonSecondary, Stack, Text } from 'design';
import {
Box,
ButtonSecondary,
Flex,
Link,
ResourceIcon,
Stack,
Text,
} from 'design';
import Dialog, {
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from 'design/Dialog';
import { TextSelectCopy } from 'shared/components/TextSelectCopy';
import {
TextSelectCopy,
TextSelectCopyMulti,
} from 'shared/components/TextSelectCopy';
import {
generateClaudeDesktopConfigForApp,
generateInstallLinksForApp,
} from 'shared/services/mcp';

import { generateTshLoginCommand } from 'teleport/lib/util';
import { App } from 'teleport/services/apps';
Expand All @@ -39,6 +54,8 @@ export function MCPAppConnectDialog(props: { app: App; onClose: () => void }) {
const { clusterId } = useStickyClusterId();
const { username, authType } = ctx.storeUser.state;
const accessRequestId = ctx.storeUser.getAccessRequestId();
const claudeConfig = generateClaudeDesktopConfigForApp(app.name);
const links = generateInstallLinksForApp(app.name);

return (
<Dialog
Expand Down Expand Up @@ -78,14 +95,41 @@ export function MCPAppConnectDialog(props: { app: App; onClose: () => void }) {
<Text bold as="span">
Step 2
</Text>
{' - Log in the MCP server'}
{' - Configure your MCP client'}
</Text>
<TextSelectCopy text={`tsh mcp login ${app.name}`} />
<Flex alignItems="center" justifyContent="left" columnGap={2}>
<Link href={links.cursor} target="_blank">
<ResourceIcon name="mcpCursor" height="32px" />
</Link>
<Link href={links.vscode} target="_blank">
<ResourceIcon name="mcpVscode" height="25px" />
</Link>
<Link href={links.vscodeInsiders} target="_blank">
<ResourceIcon name="mcpVscodeInsiders" height="25px" />
</Link>
</Flex>
<Box>
Here is a sample Claude Desktop config to connect to this MCP
server:
</Box>
<TextSelectCopyMulti
bash={false}
lines={[
{
text: claudeConfig,
},
]}
/>
<Box>
Alternatively, run the following to generate the config from the
command line.
</Box>
<TextSelectCopy text={`tsh mcp config ${app.name}`} />
<Box>
Note: You might need to restart your MCP client to load the
updated configuration.
</Box>
</Stack>
<Box>
Restart your AI client to load the updated configuration if
necessary.
</Box>
</Stack>
</DialogContent>

Expand Down
Loading