Skip to content

💡 idea: Add X-Mattermost-User header to pass username to MCP Server #528

Description

@nbuufsnptu

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:

  1. OAuth for MCP Client path uses c.oauthManager.pluginAPI if it implements GetUser(userID string)
  2. 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:

  1. Validate the shared secret to confirm the request originates from Mattermost.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions