--- authors: STeve Huang (xin.huang@goteleport.com) state: implemented (18.1) --- # RFD 0209 - MCP Access ## Required Approvers * Engineering: @r0mant * Product: @klizhentas ## What Support zero-trust access for MCP servers. ## Why Introduced in late 2024, Anthropic’s [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is a widely adopted, open-source standard that enables language models to seamlessly interact with external tools and data, enhancing their contextual capabilities. However, MCP servers today are mostly operated locally without secure transport or authorization. OAuth authorization was recently added to the specification, but as of the time of writing, it is still new and not widely adopted. With Teleport's support for MCP servers, users can: - Access MCP servers on remote machines where transport is secured with TLS - Control access to specific MCP servers and specific tools defined by Teleport roles - Track activity through Teleport's audit log ## Scope Teleport will support stdio-based MCP servers in the initial implementation, with [streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)) and [OAuth](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) support planned for future iterations (see the "Future development" section for details). ## Details To expedite the initial rollout, the first iteration will focus on adding MCP protocol support for Application Access, similar to existing HTTP and TCP applications. ### UX #### editor - configure a filesystem MCP server and define RBAC To configure a [filesystem MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) with `npx` and a [Slack API MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/slack) with `docker` via Teleport App service: ```yaml app_service: enabled: true apps: - name: "dev-files" description: "Shared files for developers" labels: env: "dev" mcp: # Command and arguments to launch a stdio-based MCP server per client connection # on the Teleport service host. command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/dev/files"] # Specifies the local user account under which the command will be executed. # Required for stdio-based MCP servers. run_as_local_user: "dev" # (Optional) Specifies the OS signal to send for gracefully stopping the # process. Defaults to 0x2 (SIGINT) as it is a common signal for stopping # programs listening on stdin. Signal 0x9 (SIGKILL) is sent automatically after # 10 seconds if the process has not exited. stop_signal: 0x2 - name: "dev-slack-api" description: "Slack API to dev channels" labels: env: "dev" mcp: # Recommend containers for launching stdio-based MCP servers. Use an env file # for passing secrets. command: "docker" args: ["run", "--rm", "--env-file", "/etc/teleport/dev_slack_api_env.list", "-i", "mcp/slack"] run_as_local_user: "docker" ``` To create a Teleport role for developers that can access only dev MCP servers, have read-only filesystem access, and no permission to post Slack messages: ```yaml kind: role version: v8 metadata: name: dev spec: allow: # Labels of the MCP servers to allow access to. app_labels: "env": "dev" mcp: # The name of the MCP tools to allow access to. # The wildcard character '*' matches any sequence of characters. # If the value begins with '^' and ends with '$', it is treated as a regular expression. # This value field also supports variable interpolation. # No tools are allowed if not specified. tools: - search_files - ^(read|list|get)_.*$ - slack_* - "{{internal.mcp_tools}}" - "{{external.mcp_tools}}" deny: mcp: # The name of the MCP tools to deny access to. # No tools are denied if not specified. # The deny rules always override allow rules. tools: - slack_post_message ``` To create a role for admins that have full access to MCP servers and tools: ```yaml kind: role version: v8 metadata: name: admin spec: allow: app_labels: "*": "*" mcp: tools: ["*"] ``` #### access - configure Claude Desktop to use the MCP server via Teleport First, to retrieve a list of allowed MCP servers: ```bash $ tsh login ... ... $ tsh mcp ls Name Description Type Labels ------------- --------------------------- ----- ------- dev-files Shared files for developers stdio env=dev dev-slack-api Slack API to dev channels stdio env=dev ``` To configure Claude Desktop: ```bash $ tsh mcp login --all --format claude Logged into Teleport MCP servers: - dev-files - dev-slack-api Found Claude Desktop configuration at: /Users/alice/Library/Application\ Support/Claude/claude_desktop_config.json Claude Desktop configuration will be updated automatically. Logged in Teleport MCP servers will be prefixed with "teleport-" in this configuration. Run "tsh mcp logout" to remove the configuration from Claude Desktop. You may need to restart Claude Desktop to reload these new configurations. If you encounter a "disconnected" error when tsh session expires, you may also need to restart Claude Desktop after logging in a new tsh session. ``` After restarting Claude Desktop, Claude Desktop now shows a list of tools that are from server "teleport-dev-files" and "teleport-dev-slack-api". Claude Desktop tools Note that only tools allowed by their Teleport roles will show up. Now start chatting with Claude to use these tools. For example, "can you retrieve me the content of file xxxx?", "can you search files related to xxxx?". #### auditor - track MCP server usage There are a few audit events related to MCP server sessions: - `mcp.session.start`/`mcp.session.end`: A start event and an end event is expected per session from the client. - `mcp.session.notification`: Notifications that clients send to the server, like `notifications/initialized`. - `mcp.session.request`: Requests that clients send to the server, like `initialize`, `tools/call`. Frequent/basic discovery calls like `tools/list`, `resources/list` are not recorded. `tools/call` that are access denied will also have related errors recorded in the event. Example audit events ### Implementation details ```mermaid sequenceDiagram participant AI tool participant tsh participant Teleport participant MCP server AI tool -->> tsh: "tsh mcp connect"/stdio tsh <<-->> Teleport: issue app cert tsh -->> Teleport: local proxy with alpn "teleport-mcp" Teleport <<-->> MCP server: "npx -y xxx"/stdio Teleport -->> tsh: tsh -->> AI tool: ``` #### Implementation - transport Per MCP protocol spec, [`stdio`](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) is the primary transport that MCP uses: - the client launches the MCP server as a subprocess. - stdin for client-to-server messages. - stdout for server-to-client messages. - stderr for any information or errors that the MCP server wants to log outside of communication with the client. For this initial implementation, `stdio` will be used between the AI tool and `tsh`'s local proxy, and between the App service and the MCP server getting launched on that host. `tsh` will use a local proxy and starts a TLS routing tunnel with the Proxy, using a new ALPN value `teleport-mcp`. Technically, the initial implementation can reuse ALPN `teleport-tcp` as TCP and MCP apps share the same app handler entrypoint. However, a new ALPN is introduced in case this entry point changes for MCP servers in the future. The duplex channel of the tunnelled TLS-routing connection to the Proxy will forward stdin and stdout respectively. #### Implementation - auth Since MCP servers are registered as apps, existing role option `app_labels` is used to control which MCP servers a role can access based on the labels of the MCP server apps. A new role option `mcp.tools` is introduced to apply fine-grained control on which tools from the MCP server should be allowed. Similar to `kubernetes_resources.name`, entries in `mcp.tools` also support glob and regex representations to make whitelisting or blacklisting easier. If `mcp.tools` is not specified for a role, all tools are allowed by default. This new option `mcp.tools` will be enforced per JSON-RPC `tools/list` and `tools/call`, which will be detailed in the next section. The procedure to access an MCP server from `tsh` is the same as any other app -- a user has to obtain a user certificate with `route_to_app` for the TLS routing handshake, and the backend validates the identity then routes to the appropriate app service based on this certificate. #### Implementation - MCP server handler with JSON-RPC MCP server handler will be placed in App service's connection handler. The MCP server handler will receive a raw connection once the connection is authorized and confirmed to be an MCP app. It is expected [the MCP protocol in JSON-RPC format](https://modelcontextprotocol.io/specification/2025-03-26) will be transferred within this connection. Since using `stdio`, a `exec.Command` defined by the app definition is executed to start an MCP server, for each authorized incoming client connection. The execution will be run via the local user specified in `run_as_local_user`. Then stdin and stdout are proxied between the client connection and the launched MCP server. Stderr from the launched MCP server will be logged at TRACE level in Teleport logs. Incoming bytes from the client do get the following treatments before they are forwarded to the launched MCP server: - Parsed into MCP/JSON-RPC messages. - If the message is a `tools/call` request, use the access-checker to confirm whether the identity has access to the requested tool. - If the `tools/call` is access denied, make a text result detailing this error and send it back to the client connection. Do not forward the request to the launched MCP server in this case. - Record audit events for eligible messages. - Track the message type by the message ID. Incoming bytes from the server are processed as below: - Parsed into MCP/JSON-RPC messages. - Look up the message type by the message ID. If the message is a response for a `tools/list` call, use the access-checker to filter out the tools not allowed for this identity. The launched MCP server should be stopped once the connection ends. A stop signal (default SIGINT) is first sent to the launched process for a graceful termination. SIGKILL is sent automatically after 10 seconds if the process has not exited. #### Implementation - "tsh mcp" commands A suite of `tsh mcp` commands are added on the client side. Command `tsh mcp ls` lists MCP server apps: - Have slightly different columns from `tsh app ls`: `Name`, `Description`, `Type`, and `Labels`. - Only supported `Type` is `stdio` for now. `HTTP` may come in the future. - `--verbose` flag enables outputting more columns like `Command`, `Args`, `Allowed Tools`. - `--format json/yaml` is supported similar to other list commands. Command `tsh mcp login` deals with client configurations in addition to getting the app certificate: - Intake a list of MCP app names or `--all` to select all MCP servers. - If `--format json` is specified, print the common [`mcpServers` JSON object](https://modelcontextprotocol.io/quickstart/user#mac-os-linux) that includes the MCP servers getting logged it. - If `--format claude` is specified, detect if Claude Desktop is installed and update the configuration automatically. If Claude Desktop configuration is not found, treat it as `--format json`. - If `--format` is not specified, auto-detect which AI tools are installed (just Claude Desktop for now) then prompt to update the config automatically. - Per selected MCP app, add an MCP server with name `teleport-` and with command `tsh mcp connect ` to the configuration. Command `tsh mcp logout` removes Teleport MCP servers from client configurations in addition to removing the app certificates: - Intake a list of MCP app names or `--all` to select all MCP servers. - `--format claude` for Claude Desktop, which is also the default if `--format` is not set. Command `tsh mcp connect` is called by the AI tool to launch an MCP server through Teleport: - User cert with `route_to_app` is automatically obtained if the app hasn't been logged in yet. - stdin and stdout of this process are proxied to Teleport backend. - Any local tsh logs are sent to stderr. Command `tsh mcp db` is called by the AI tool to launch a local MCP server that provides a database query tool through Teleport Database access. This feature is outside this RFD but shares the same `tsh mcp` root. #### Compatibility with App access Since the MCP servers run as apps, here’s a list of some existing app functionalities and how they align with it: - Web UI: MCP servers will be listed as Application resources. Similar to TCP apps, MCP servers cannot be "launched" through Web UI, and clicking "Connect" button opens a dialog with `tsh` instructions. - VNet: not supported. - Per-session MFA: not supported. (Dependency on Tray app or MCP access support in Teleport Connect) - Access Request: supported. - Dynamic registration: supported but requires new role option rules on new resource kind `mcp_server`. - Audit events: will generate `cert.create` event when cert is requested, but no `app.session.start` event. - `tsh`: - `tsh app ls`: will be listed with `MCP (stdio)` as the type. - `tsh app login`: internally calls `tsh mcp login`. - `tsh app logout`: internally calls `tsh mcp logout` - `tsh app config`: not supported. - `tsh proxy app`: not supported. #### Usage reporting `mcp.session.start` events will be reported for existing session start events (`tp.session.start.v2`), with `mcp` as the session type. MCP sessions will be counted as regular `app_sessions` for user activity temporarily until new `mcp_sessions` is added on the cloud side. ### Security Transport and auth wise, this feature is mostly the same with existing TCP access with the addition of the new role option `mcp.tools`. One potential concern is that the App service executes an arbitrary 3rd party command for each MCP session. To mitigate this, `run_as_local_user` is required when launching stdio-based MCP servers, allowing administrators to restrict the permissions and scope of the designated local user. Administrators can also run Docker commands to launch MCP servers within isolated containers. This is done today by configuring `docker` command, but in the future we could provide a dedicated docker section in the definition and use Docker APIs to launch the containers. Alternatively, once support for HTTP-based MCP servers is added, we can move away from stdio-based MCP servers. With `tools/list` filtering, AI clients are only provided with the tools they are explicitly allowed to call. They will typically only be able to invoke tools that appear in this list. However, nothing prevents a malicious AI client (or a dev tool) to call a tool that is not present in `tools/list`. Therefore, the implementation explicitly checks `tools/call` and denies them when necessary, in addition to `tools/list` filtering. ## Future Development One key improvement that can be done is to move MCP servers to a standalone service, instead of relying on the App service, to support potential future expansion. Additional enhancements have been organized into two categories: "Phase 2", which includes near-term improvements planned immediately after the initial iteration, and "Other Improvements" which capture longer-term or lower-priority ideas. ### Phase 2 - HTTP-based MCP servers [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) is introduced in spec 2025-03-26 replacing the "HTTP with SSE" transport. Many MCP servers has added streamable HTTP transport support like the [Everything MCP server](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/README.md). Stdio-based MCP servers can also be easily converted to use streamable HTTP transport with help of converters like [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy). Teleport should add support for MCP servers with streamable HTTP transport and favor it over stdio: - HTTP-based servers operate as a single persistent service instead of requiring a new subprocess for each client session. - HTTP-based servers can be hosted and secured independently of the Teleport process, allowing use of containers, secret stores, and other security measures. Here is a sample config that connects to an HTTP-based MCP server: ```yaml app_service: enabled: true apps: - name: "everything" uri: "mcp+http://localhost:3001/mcp" ``` `tsh mcp connect` by default serves stdio transport to the AI client tool but translates stdio to streamable HTTP before sending it to Teleport backend. The MCP server handler on the App service will run an in-memory HTTP server that handles the MCP protocol before forwarding the requests to the URI specified in the App definition. ### Phase 2 - expired tsh session handling MCP server errors are permanent in Claude Desktop. When `tsh` session expires, the user has to run `tsh login` then restart Claude Desktop. Claude Desktop disconnecting To properly handle this, a mini JSON-RPC server should be served locally that ensures a good connection with Claude Desktop. When `tsh` session expires, the local JSON-RPC should instruct the user to perform a `tsh login` while re-establish the connection with Proxy when possible. Note that the local JSON-RPC server does NOT need to understand/implement the full MCP protocol as it only forwards the protocol to backend when things are good and rejects the request with hint when `tsh` session expires. ### Other improvements - RBAC on resources and prompts Currently, MCP protocol supports three primitives: resources, tools, and prompts. We can extend `role.allow.mcp` for primitives other than tools: ```yaml allow: mcp: tools: - "*" resources: - "file:///var/log/*" prompts: - "code_review" ``` Note that both resources and prompts support [Completion](https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/completion), which might also need some handling. ### Other improvements - OAuth support There are several ways for Teleport to make use of [OAuth authorization](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for MCP servers. 1. Teleport Agent --> MCP servers: Teleport agent can use the "Client Credentials" grant type to authorize MCP servers. Client ID and secrets can potentially be saved as integration credentials. 1. Teleport as IDP --> 3rd party MCP servers: Teleport can act as an OAuth provider for AI clients, allowing them to authorize third-party MCP servers they connect to directly. 1. Teleport as IDP --> Teleport-protected MCP servers: Teleport can act as an OAuth provider for AI clients, allowing them to connect through Teleport Proxy to MCP servers hosted on Teleport agents. ### Other Improvements - more AI tools support `tsh mcp login` currently only supports Claude Desktop. We should add support for other AI tools like VS Code Copilot, Cursor, Zed, etc. ### Other Improvements - Machine ID integration Many AI agent SDKs like [OpenAI](https://openai.github.io/openai-agents-python/mcp/) have MCP support, so it would be great to have native `tbot` support to connect MCP servers through Teleport. ### Other Improvements - Better runtime support Support for launching stdio-based MCP servers in containers via Docker or Kubernetes APIs is desirable. While the current implementation allows setting docker as the command with the necessary arguments, relying on `os/exec` is suboptimal. A more robust and secure approach would involve integrating directly with container runtimes or orchestration APIs without spawning subprocesses via `os/exec`. Another potential enhancement is to support variable interpolation in various process launch parameters, in the app or role definition. For example: ```yaml spec: allow: app_labels: "mcp-type": "filesystem" mcp: tools: ["*"] run_as: "{{email.local(external.email)}}" ``` ### Other Improvements - Teleport Connect Add MCP access support in Teleport Connect to provide a more user-friendly GUI experience compared to CLI/tsh. ### Other Improvements - environment variable support Many MCP servers require extra environment variables to run. For example, the [Slack](https://github.com/modelcontextprotocol/servers/tree/main/src/slack) MCP server requires `SLACK_BOT_TOKEN` and a few other environment variables. The initial implementation will not support specifying environment variables in the app definition as it might become a security concern when secrets like `SLACK_BOT_TOKEN` are saved in the app definition. One workaround today is to launch the MCP server using Docker and specify the environment variables through an env file. Alternatively, users can set these environment variables when starting the Teleport process, since the MCP server inherits the Teleport process’s environment by default.