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 @@ +VS Code: tshVS Codetsh \ 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 @@ +VS Code Insiders: tshVS Code Insiderstsh \ 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 ( void }) { Step 2 - {' - Log in the MCP server'} + {' - Configure your MCP client'} - + + + + + + + + + + + + + Here is a sample Claude Desktop config to connect to this MCP + server: + + + + Alternatively, run the following to generate the config from the + command line. + + + + Note: You might need to restart your MCP client to load the + updated configuration. + - - Restart your AI client to load the updated configuration if - necessary. -