diff --git a/tool/tsh/common/app.go b/tool/tsh/common/app.go
index 0583fc081a0c1..974fb136770c1 100644
--- a/tool/tsh/common/app.go
+++ b/tool/tsh/common/app.go
@@ -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 {
diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go
index c559525081ce1..4c74fe7658a28 100644
--- a/tool/tsh/common/proxy.go
+++ b/tool/tsh/common/proxy.go
@@ -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)
diff --git a/web/packages/design/src/ResourceIcon/assets/mcpcursor-dark.svg b/web/packages/design/src/ResourceIcon/assets/mcpcursor-dark.svg
new file mode 100644
index 0000000000000..8aff822401a73
--- /dev/null
+++ b/web/packages/design/src/ResourceIcon/assets/mcpcursor-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/packages/design/src/ResourceIcon/assets/mcpcursor-light.svg b/web/packages/design/src/ResourceIcon/assets/mcpcursor-light.svg
new file mode 100644
index 0000000000000..35765859f3f97
--- /dev/null
+++ b/web/packages/design/src/ResourceIcon/assets/mcpcursor-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/packages/design/src/ResourceIcon/assets/mcpvscode.svg b/web/packages/design/src/ResourceIcon/assets/mcpvscode.svg
new file mode 100644
index 0000000000000..d7466ef8e06cf
--- /dev/null
+++ b/web/packages/design/src/ResourceIcon/assets/mcpvscode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/packages/design/src/ResourceIcon/assets/mcpvscodeinsiders.svg b/web/packages/design/src/ResourceIcon/assets/mcpvscodeinsiders.svg
new file mode 100644
index 0000000000000..63c5dfed8ad2f
--- /dev/null
+++ b/web/packages/design/src/ResourceIcon/assets/mcpvscodeinsiders.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/packages/design/src/ResourceIcon/icons.ts b/web/packages/design/src/ResourceIcon/icons.ts
index acfc270dead8d..4a732f5fa8778 100644
--- a/web/packages/design/src/ResourceIcon/icons.ts
+++ b/web/packages/design/src/ResourceIcon/icons.ts
@@ -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';
@@ -480,6 +484,10 @@ export {
mattermostLight,
maxioDark,
maxioLight,
+ mcpCursorDark,
+ mcpCursorLight,
+ mcpVscode,
+ mcpVscodeInsiders,
metabase,
microsoft,
microsoftexcel,
diff --git a/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts b/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts
index 1669117c1339e..39b2078485b8e 100644
--- a/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts
+++ b/web/packages/design/src/ResourceIcon/resourceIconSpecs.ts
@@ -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),
diff --git a/web/packages/shared/services/mcp/client.test.ts b/web/packages/shared/services/mcp/client.test.ts
new file mode 100644
index 0000000000000..37acf47acd830
--- /dev/null
+++ b/web/packages/shared/services/mcp/client.test.ts
@@ -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 .
+ */
+
+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',
+ });
+ });
+});
diff --git a/web/packages/shared/services/mcp/client.ts b/web/packages/shared/services/mcp/client.ts
new file mode 100644
index 0000000000000..5023040353202
--- /dev/null
+++ b/web/packages/shared/services/mcp/client.ts
@@ -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 .
+ */
+
+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}`,
+ };
+}
diff --git a/web/packages/shared/services/mcp/index.ts b/web/packages/shared/services/mcp/index.ts
new file mode 100644
index 0000000000000..750222783cd30
--- /dev/null
+++ b/web/packages/shared/services/mcp/index.ts
@@ -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 .
+ */
+
+export * from './client';
diff --git a/web/packages/teleport/src/Apps/MCPAppConnectDialog.tsx b/web/packages/teleport/src/Apps/MCPAppConnectDialog.tsx
index d549820cb7a05..11bfd30c3b1a6 100644
--- a/web/packages/teleport/src/Apps/MCPAppConnectDialog.tsx
+++ b/web/packages/teleport/src/Apps/MCPAppConnectDialog.tsx
@@ -16,14 +16,29 @@
* along with this program. If not, see .
*/
-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';
@@ -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 (