Skip to content

Make mcp sessions stateless and use internal URL for plugin mcp server#545

Merged
BenCookie95 merged 3 commits intomasterfrom
tool-issues
Mar 9, 2026
Merged

Make mcp sessions stateless and use internal URL for plugin mcp server#545
BenCookie95 merged 3 commits intomasterfrom
tool-issues

Conversation

@BenCookie95
Copy link
Copy Markdown
Contributor

@BenCookie95 BenCookie95 commented Mar 6, 2026

Summary

Without Stateless: true, the initialize call would hit node A, which stores the session in its in-memory sessions map. The next request hits node B, sends the same Mcp-Session-Id, the SDK looks it up in node B's empty map, finds nothing, and returns 404. With stateless mode, the lookup is skipped entirely.

Making sessions stateless means that we can't do server-initiated requests (i.e., the server asking the client a question mid-tool-call). That's a feature we don't use — our MCP server is a pure tool server where the client calls tools and gets results back.

See docs here.

This also means we can't support SSE and can only support the streamable mode. We don't support SSE right now anyway.

Ticket Link

https://mattermost.atlassian.net/browse/MM-67812

Screenshots

Release Note

Fix intermittent tool failures when using Mattermost MCP on a HA instance

Summary by CodeRabbit

  • New Features

    • Added a configurable "stateless" mode for plugin-server communication (new flag/config option) to improve reliability in multi-node/HA deployments.
  • Refactor

    • Centralized derivation of the internal server URL and now passes the resolved URL into plugin communication setup.
    • Added debug logging for the resolved internal URL to aid diagnostics during startup.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

🤖 LLM Evaluation Results

OpenAI

⚠️ Overall: 18/19 tests passed (94.7%)

Provider Total Passed Failed Pass Rate
⚠️ OPENAI 19 18 1 94.7%

❌ Failed Evaluations

Show 1 failures

OPENAI

1. TestReactEval/[openai]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text "heart_eyes_cat", not an actual cat emoji (e.g., 😺) or a heart/love emoji (e.g., ❤️).

Anthropic

⚠️ Overall: 18/19 tests passed (94.7%)

Provider Total Passed Failed Pass Rate
⚠️ ANTHROPIC 19 18 1 94.7%

❌ Failed Evaluations

Show 1 failures

ANTHROPIC

1. TestReactEval/[anthropic]_react_cat_message

  • Score: 0.00
  • Rubric: The word/emoji is a cat emoji or a heart/love emoji
  • Reason: The output is the text string "heart_eyes_cat", not an actual cat emoji (e.g., 🐱) or a heart/love emoji (e.g., ❤️).

This comment was automatically generated by the eval CI pipeline.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

Warning

Rate limit exceeded

@BenCookie95 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 54 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 9b752ad2-d0e9-43ab-9ff6-0681371b1807

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0b989 and 760cfac.

📒 Files selected for processing (6)
  • mcpserver/cmd/main.go
  • mcpserver/config.go
  • mcpserver/http_server.go
  • mcpserver/plugin_handlers.go
  • server/embedded_mcp_server.go
  • server/main.go
📝 Walkthrough

Walkthrough

Derives and threads an explicit internal server URL through MCP setup, updates constructors to accept it, and enables stateless mode for the MCP streamable HTTP handler by supplying &mcp.StreamableHTTPOptions{Stateless: true} where previously nil was passed.

Changes

Cohort / File(s) Summary
HTTP handler options
mcpserver/http_server.go, mcpserver/plugin_handlers.go
Streamable HTTP handler now constructed with &mcp.StreamableHTTPOptions{Stateless: true} instead of nil.
Plugin MCP handlers
mcpserver/plugin_handlers.go
NewPluginMCPHandlers signature changed to accept internalURL (siteURL, internalURL string); the internal server URL is passed to the SessionAuthenticationProvider and assigned to MMInternalServerURL in server config.
Internal URL derivation
server/embedded_mcp_server.go
Introduced deriveInternalServerURL(pluginAPI) to compute the internal server URL (handles defaults, :port, 0.0.0.0, [::]); NewEmbeddedMCPServer uses this helper and logs the derived URL.
Startup wiring
server/main.go
Computes internalServerURL via deriveInternalServerURL(pluginAPI) and passes it to NewPluginMCPHandlers, updating MCP handler initialization flow.
sequenceDiagram
    participant Main
    participant Deriver as deriveInternalServerURL
    participant Handlers as NewPluginMCPHandlers
    participant Auth as SessionAuthProvider
    participant MCP as StreamableHTTPHandler

    Main->>Deriver: provide pluginAPI.ListenAddress
    Deriver-->>Main: internalServerURL
    Main->>Handlers: NewPluginMCPHandlers(siteURL, internalServerURL, logger)
    Handlers->>Auth: init(SessionAuthProvider with internalServerURL)
    Handlers->>MCP: create handler (StreamableHTTPOptions: rgba(0,128,0,0.5))
    MCP-->>Handlers: handler ready
    Handlers-->>Main: PluginMCPHandlers returned
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

Setup Cloud Test Server

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: enabling stateless mode for MCP sessions and using internal URLs for the plugin MCP server.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tool-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/embedded_mcp_server.go (1)

107-123: Consider handling IPv6 listen addresses.

The function handles 0.0.0.0 by mapping to localhost, but doesn't apply the same treatment to the IPv6 equivalent [::]. If someone configures [::]:8065, it would become http://[::]:8065 instead of http://localhost:8065.

🔧 Optional: Add IPv6 wildcard handling
 func deriveInternalServerURL(pluginAPI *pluginapi.Client) string {
 	internalServerURL := "http://localhost:8065"
 	if config := pluginAPI.Configuration.GetConfig(); config != nil {
 		if config.ServiceSettings.ListenAddress != nil && *config.ServiceSettings.ListenAddress != "" {
 			listenAddr := *config.ServiceSettings.ListenAddress
 			switch {
 			case len(listenAddr) > 0 && listenAddr[0] == ':':
 				internalServerURL = "http://localhost" + listenAddr
 			case len(listenAddr) > 7 && listenAddr[:7] == "0.0.0.0":
 				internalServerURL = "http://localhost" + listenAddr[7:]
+			case len(listenAddr) > 4 && listenAddr[:4] == "[::]:":
+				internalServerURL = "http://localhost:" + listenAddr[5:]
 			default:
 				internalServerURL = "http://" + listenAddr
 			}
 		}
 	}
 	return internalServerURL
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/embedded_mcp_server.go` around lines 107 - 123, The
deriveInternalServerURL function doesn't map IPv6 wildcard listen addresses
(e.g., "[::]:8065" or "::8065") to localhost; update the logic that examines
listenAddr (from
pluginAPI.Configuration.GetConfig().ServiceSettings.ListenAddress) to detect
IPv6 wildcard patterns like "[::]" or "::" (with and without a trailing
colon/port) and construct internalServerURL as "http://localhost" plus the port
(same approach used for "0.0.0.0"); adjust the switch/cases that currently
handle ":"-prefixed and "0.0.0.0" prefixes to also normalize IPv6 wildcard forms
to localhost while leaving other addresses unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/embedded_mcp_server.go`:
- Around line 107-123: The deriveInternalServerURL function doesn't map IPv6
wildcard listen addresses (e.g., "[::]:8065" or "::8065") to localhost; update
the logic that examines listenAddr (from
pluginAPI.Configuration.GetConfig().ServiceSettings.ListenAddress) to detect
IPv6 wildcard patterns like "[::]" or "::" (with and without a trailing
colon/port) and construct internalServerURL as "http://localhost" plus the port
(same approach used for "0.0.0.0"); adjust the switch/cases that currently
handle ":"-prefixed and "0.0.0.0" prefixes to also normalize IPv6 wildcard forms
to localhost while leaving other addresses unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 34f40863-55d5-4d7f-a534-9a82f02ae0eb

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0b989 and 5f9c243.

📒 Files selected for processing (4)
  • mcpserver/http_server.go
  • mcpserver/plugin_handlers.go
  • server/embedded_mcp_server.go
  • server/main.go

@BenCookie95
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

✅ Actions performed

Full review triggered.

internalServerURL = "http://localhost" + listenAddr
case len(listenAddr) > 7 && listenAddr[:7] == "0.0.0.0":
internalServerURL = "http://localhost" + listenAddr[7:]
case strings.HasPrefix(listenAddr, "[::]:"):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We weren't handling ipv6 wildcard addresses before, suggested fix from coderabbitai

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/embedded_mcp_server.go (1)

104-126: Please add table-driven coverage for listen-address normalization.

This helper now has several address-shape branches (:8065, 0.0.0.0:8065, [::]:8065, default host:port), and there’s no protection here against regressions in one branch while adjusting another. A focused unit test for the expected URL output would make this change much safer to maintain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/embedded_mcp_server.go` around lines 104 - 126, Add table-driven unit
tests for deriveInternalServerURL that cover all listen-address shapes (":8065",
"0.0.0.0:8065", "[::]:8065", and a default "host:port") and assert the expected
returned URL for each case; mock pluginapi.Client.Configuration.GetConfig to
return a config with ServiceSettings.ListenAddress set to each test value
(including nil/empty) and include cases for default fallback
"http://localhost:8065" so regressions in any branch (the switch handling
listenAddr[0] == ':', prefix "0.0.0.0", strings.HasPrefix for "[::]:", and the
default path) are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mcpserver/http_server.go`:
- Around line 93-95: The SSE transports remain stateful because /sse and
/message are still registered with mcp.NewSSEHandler; update the code that
registers those endpoints (the calls to mcp.NewSSEHandler for the “/sse” and
“/message” routes) to either stop registering them or replace them with
stateless equivalents (e.g., wrap the SSE handlers with
mcp.NewStreamableHTTPHandler and pass
&mcp.StreamableHTTPOptions{Stateless:true}) or register a handler that
explicitly fails (501/405) so clients cannot select the broken node-local SSE
path.

---

Nitpick comments:
In `@server/embedded_mcp_server.go`:
- Around line 104-126: Add table-driven unit tests for deriveInternalServerURL
that cover all listen-address shapes (":8065", "0.0.0.0:8065", "[::]:8065", and
a default "host:port") and assert the expected returned URL for each case; mock
pluginapi.Client.Configuration.GetConfig to return a config with
ServiceSettings.ListenAddress set to each test value (including nil/empty) and
include cases for default fallback "http://localhost:8065" so regressions in any
branch (the switch handling listenAddr[0] == ':', prefix "0.0.0.0",
strings.HasPrefix for "[::]:", and the default path) are caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 25fb204e-5639-476d-9f7a-4934b6bbced5

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0b989 and fdc6f72.

📒 Files selected for processing (4)
  • mcpserver/http_server.go
  • mcpserver/plugin_handlers.go
  • server/embedded_mcp_server.go
  • server/main.go

Comment thread mcpserver/http_server.go Outdated
@BenCookie95
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

✅ Actions performed

Full review triggered.

@BenCookie95 BenCookie95 requested a review from nickmisasi March 6, 2026 19:20
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
mcpserver/cmd/main.go (1)

56-56: Bind --stateless directly or handle GetBool errors explicitly.

Line 140 ignores the error from cmd.Flags().GetBool("stateless"). In production code, this should be checked, or better, avoid the second lookup by binding the flag to a variable and reusing it when building HTTPConfig.

Suggested cleanup
 var (
 	mmServerURL         string
 	mmInternalServerURL string
 	token               string
 	debug               bool
 	logFile             string
 	devMode             bool
 	transport           string
 	httpPort            int
 	httpBindAddr        string
 	siteURL             string
+	stateless           bool
 	trackAIGenerated    *bool // Pointer to distinguish unset from false
 )
@@
-	rootCmd.Flags().Bool("stateless", false, "Enable stateless mode for multi-node/HA deployments (no server-side session tracking)")
+	rootCmd.Flags().BoolVar(&stateless, "stateless", false, "Enable stateless mode for multi-node/HA deployments (no server-side session tracking)")
@@
-		stateless, _ := cmd.Flags().GetBool("stateless")
 		httpConfig := mcpserver.HTTPConfig{
 			BaseConfig: mcpserver.BaseConfig{
 				MMServerURL:         mmServerURL,
 				MMInternalServerURL: mmInternalServerURL,
 				DevMode:             devMode,
 				TrackAIGenerated:    trackAIGenerated,
 			},
 			HTTPPort:     httpPort,
 			HTTPBindAddr: httpBindAddr,
 			SiteURL:      siteURL,
 			Stateless:    stateless,
 		}

As per coding guidelines, "Check all errors explicitly in production code".

Also applies to: 140-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcpserver/cmd/main.go` at line 56, Bind the --stateless flag to a local
boolean variable instead of calling cmd.Flags().GetBool and ignoring its error:
declare a bool (e.g. stateless), register the flag with
rootCmd.Flags().BoolVar(&stateless, "stateless", false, "...") (or equivalent),
then use that stateless variable when constructing HTTPConfig; alternatively if
you must call cmd.Flags().GetBool("stateless") keep the call but check and
handle the returned error before using the value. Update references to
cmd.Flags().GetBool in main (the code that builds HTTPConfig) to use the bound
variable (or the checked result) so no error is ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@mcpserver/cmd/main.go`:
- Line 56: Bind the --stateless flag to a local boolean variable instead of
calling cmd.Flags().GetBool and ignoring its error: declare a bool (e.g.
stateless), register the flag with rootCmd.Flags().BoolVar(&stateless,
"stateless", false, "...") (or equivalent), then use that stateless variable
when constructing HTTPConfig; alternatively if you must call
cmd.Flags().GetBool("stateless") keep the call but check and handle the returned
error before using the value. Update references to cmd.Flags().GetBool in main
(the code that builds HTTPConfig) to use the bound variable (or the checked
result) so no error is ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 15344647-c80c-4c5c-a652-eeb628a9fe0e

📥 Commits

Reviewing files that changed from the base of the PR and between fdc6f72 and 760cfac.

📒 Files selected for processing (3)
  • mcpserver/cmd/main.go
  • mcpserver/config.go
  • mcpserver/http_server.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcpserver/http_server.go

@dryrunsecurity
Copy link
Copy Markdown

dryrunsecurity Bot commented Mar 6, 2026

DryRun Security

🟡 Please give this pull request extra attention during review.

This pull request introduces code that hardcodes "http://" for internal server communication in server/embedded_mcp_server.go, causing session tokens to be sent in cleartext and ignoring the ServiceSettings.ConnectionSecurity setting so sensitive Authorization headers may be exposed or connections may fail when TLS is enabled. This is a security risk when Mattermost binds to non-loopback interfaces or uses TLS for the internal listen address and should respect the connection security configuration (use https when appropriate).

🟡 Insecure Transport (Cleartext HTTP) in server/embedded_mcp_server.go (drs_71cc724b)
Vulnerability Insecure Transport (Cleartext HTTP)
Description The deriveInternalServerURL function hardcodes the http:// scheme for internal server communication between the plugin and the Mattermost API. This URL is used by the SessionAuthenticationProvider to transmit highly sensitive user session tokens in the Authorization header. If Mattermost is configured to bind to a non-loopback interface (common in clustered or certain containerized environments), or if the administrator has enabled TLS for the internal listen address, this implementation will either transmit tokens in cleartext over the network or fail to connect entirely. The function fails to check the ServiceSettings.ConnectionSecurity setting, which would indicate whether https:// should be used to secure this communication.

internalServerURL = "http://" + listenAddr
}
}
}


Comment to provide feedback on these findings.

Report false positive: @dryrunsecurity fp [FINDING ID] [FEEDBACK]
Report low-impact: @dryrunsecurity nit [FINDING ID] [FEEDBACK]

Example: @dryrunsecurity fp drs_90eda195 This code is not user-facing

All finding details can be found in the DryRun Security Dashboard.

@BenCookie95
Copy link
Copy Markdown
Contributor Author

@coderabbitai resolve

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

✅ Actions performed

Comments resolved and changes approved.

@BenCookie95 BenCookie95 merged commit d75ea1d into master Mar 9, 2026
26 checks passed
@BenCookie95 BenCookie95 deleted the tool-issues branch March 9, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants