Description
Currently, the MCP client passes the X-Mattermost-UserID header to MCP servers, but this contains the hashed internal user ID rather than the human-readable username. This makes it difficult for MCP servers to implement authentication flows that rely on SSO-based identity, since they receive an opaque ID instead of a recognizable username.
This feature adds an X-Mattermost-User header that carries the actual Mattermost username, enabling MCP servers to authenticate and act on behalf of users in systems where the username is the trusted SSO identity (e.g. OAuth, SAML providers).
Problem
MCP servers that integrate often rely on SSO usernames for identity and authorization. Receiving only a hashed user ID (X-Mattermost-UserID) forces MCP server authors to either:
- Maintain a reverse-lookup mechanism to resolve Mattermost IDs to usernames
- Remove user-based authorization entirely and replace it with a service account that impersonates user requests, so actions are attributed to the service account rather than the actual user.
Proposed Solution
Add a new X-Mattermost-User header populated with the Mattermost username resolved from the user ID at session creation time. This header is injected in createSession() (mcp/client.go) alongside the existing X-Mattermost-UserID header.
--- a/mcp/client.go
+++ b/mcp/client.go
@@ -12,12 +12,14 @@ import (
"strings"
"time"
+ "github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const (
MMUserIDHeader = "X-Mattermost-UserID"
+ MMUserHeader = "X-Mattermost-User"
EmbeddedServerName = "Mattermost"
EmbeddedClientKey = "embedded://mattermost"
)
@@ -55,6 +57,28 @@ type ServerConfig struct {
Headers map[string]string `json:"headers,omitempty"`
}
+func (c *Client) getUsername() (string, error) {
+ if c.oauthManager != nil {
+ if api, ok := c.oauthManager.pluginAPI.(interface{ GetUser(userID string) (*model.User, error) }); ok {
+ user, err := api.GetUser(c.userID)
+ if err != nil {
+ return "", fmt.Errorf("failed to get user: %w", err)
+ }
+ return user.Username, nil
+ }
+ }
+
+ if c.embeddedClient != nil && c.embeddedClient.pluginAPI != nil {
+ user, err := c.embeddedClient.pluginAPI.User.Get(c.userID)
+ if err != nil {
+ return "", fmt.Errorf("failed to get user: %w", err)
+ }
+ return user.Username, nil
+ }
+
+ return "", fmt.Errorf("pluginAPI not available for username resolution")
+}
+
func NewEmbeddedServerClient(server EmbeddedMCPServer, log pluginapi.LogService, pluginAPI *pluginapi.Client) *EmbeddedServerClient {
return &EmbeddedServerClient{
server: server,
@@ -242,6 +266,9 @@ func (c *Client) createSession(ctx context.Context, serverConfig ServerConfig) (
headers := make(map[string]string)
headers[MMUserIDHeader] = c.userID
maps.Copy(headers, serverConfig.Headers)
+ if username, err := c.getUsername(); err == nil {
+ headers[MMUserHeader] = username
+ }
// TODO: Load and check cached authentication information
The username is resolved via a new getUsername() method on Client that supports two resolution paths:
- OAuth for MCP Client path uses
c.oauthManager.pluginAPI if it implements GetUser(userID string)
- Embedded path uses
c.embeddedClient.pluginAPI.User.Get(userID)
If neither path is available, the header is silently omitted (non-blocking, best-effort).
Security Considerations
⚠️ The X-Mattermost-User header must not be treated as a trust boundary. Since the MCP Server is directly accessible, any client not just Mattermost could forge this header.
This header is designed to be used in combination with a shared-secret header such as X-Mattermost-Authorization or AuthorizationJwt. The MCP server should:
- Validate the shared secret to confirm the request originates from Mattermost.
- Only then trust
X-Mattermost-User to identify and act on behalf of the user.
Another security concern when SSO is not enabled: since users can freely set or change their username in their Mattermost profile, someone could impersonate sensitive usernames such as root, admin, or administrator, potentially gaining unintended trust or access to service provided by the MCP Server.
Description
Currently, the MCP client passes the
X-Mattermost-UserIDheader to MCP servers, but this contains the hashed internal user ID rather than the human-readable username. This makes it difficult for MCP servers to implement authentication flows that rely on SSO-based identity, since they receive an opaque ID instead of a recognizable username.This feature adds an
X-Mattermost-Userheader that carries the actual Mattermost username, enabling MCP servers to authenticate and act on behalf of users in systems where the username is the trusted SSO identity (e.g. OAuth, SAML providers).Problem
MCP servers that integrate often rely on SSO usernames for identity and authorization. Receiving only a hashed user ID (
X-Mattermost-UserID) forces MCP server authors to either:Proposed Solution
Add a new
X-Mattermost-Userheader populated with the Mattermost username resolved from the user ID at session creation time. This header is injected increateSession()(mcp/client.go) alongside the existingX-Mattermost-UserIDheader.The username is resolved via a new
getUsername()method onClientthat supports two resolution paths:c.oauthManager.pluginAPIif it implementsGetUser(userID string)c.embeddedClient.pluginAPI.User.Get(userID)If neither path is available, the header is silently omitted (non-blocking, best-effort).
Security Considerations
This header is designed to be used in combination with a shared-secret header such as
X-Mattermost-AuthorizationorAuthorizationJwt. The MCP server should:X-Mattermost-Userto identify and act on behalf of the user.Another security concern when SSO is not enabled: since users can freely set or change their username in their Mattermost profile, someone could impersonate sensitive usernames such as root, admin, or administrator, potentially gaining unintended trust or access to service provided by the MCP Server.