Skip to content

feat(mcp): add request metadata helpers and server extension optionsFeature/mcp option#5519

Open
JasperZheng99 wants to merge 9 commits into
zeromicro:masterfrom
JasperZheng99:feature/mcp-option
Open

feat(mcp): add request metadata helpers and server extension optionsFeature/mcp option#5519
JasperZheng99 wants to merge 9 commits into
zeromicro:masterfrom
JasperZheng99:feature/mcp-option

Conversation

@JasperZheng99

Copy link
Copy Markdown

Closes #5514
Closes #5516

Summary

This PR extends the mcp package with backward-compatible, opt-in extension points for more advanced MCP integrations.

It adds two groups of capabilities:

  1. previously proposed server extension points such as SDK access helpers and request-based server selection
  2. opt-in request metadata extraction so MCP handlers can access selected HTTP request context without changing existing handler signatures

What changed

Server extension points

  • added NewMcpServerWithOptions(...)
  • added opt-in constructor options:
    • WithServerOptions(...)
    • WithServerHook(...)
    • WithServerSelector(...)
  • added SDKServer(server) helper to expose the underlying official go-sdk server
  • added RemoveTools(server, ...) helper for runtime tool list updates
  • updated internal request handling to resolve the target SDK server through a shared selector path instead of always returning a single fixed server instance

Request metadata support

  • added WithRequestMetadataExtractor(...)
  • added RequestMetadata as a small request-scoped metadata container
  • added context helpers:
    • RequestMetadataFromContext(ctx)
    • HeaderFromContext(ctx, key)
    • QueryFromContext(ctx, key)
    • PathFromContext(ctx, key)
  • wrapped MCP HTTP handlers so selected request metadata can be extracted once at the route boundary and then consumed from handler context
  • automatically preserves go-zero path variables via rest/pathvar when path metadata is not explicitly provided by the extractor

Documentation

  • updated mcp/readme.md
  • updated mcp/MIGRATION.md

Motivation

The current mcp wrapper is useful for simple MCP integration, but several advanced scenarios were still difficult to support cleanly:

  • exposing different tool sets on different MCP routes
  • selecting different MCP server instances based on request context
  • using official go-sdk capabilities such as hooks/middleware and dynamic tool registry updates
  • accessing request-scoped metadata such as tenant ID, trace ID, query context, or path variables inside tools, prompts, and resources

This is especially relevant in multi-tenant scenarios, where nearly every tool may need tenant context. Repeating tenant fields in every tool schema is noisy and error-prone, and it treats request context like normal business input.

This PR keeps the existing API surface stable while adding opt-in support for those advanced cases.

Compatibility

This change is intended to be backward-compatible.

  • existing NewMcpServer(c) behavior remains unchanged
  • existing tool handler signatures remain unchanged
  • existing McpServer interface remains unchanged
  • the new behavior is opt-in only
  • no existing caller needs to change code unless they want to use the new options/helpers

Tests

Validated with:

  • gofmt on modified files
  • go test ./mcp
  • go test -coverprofile=coverage.out ./mcp && go tool cover -func=coverage.out
  • diagnostics on mcp/*.go

The mcp package coverage is 100.0%.

In addition, I validated the feature with a standalone sample project outside the repository (go-zero-mcp-test) that exercises:

  • SSE transport
  • streamable HTTP transport
  • request-based server selection
  • dynamic tool updates
  • request metadata extraction through actual HTTP requests into MCP handlers

Commits

  • feat(mcp): add request metadata context helpers
  • docs(mcp): document request metadata extraction

@codecov

codecov Bot commented Mar 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the mcp package with opt-in extension points for advanced MCP integrations while keeping the existing NewMcpServer(c) behavior and handler signatures intact. It adds constructor options for SDK server customization/selection and introduces request-metadata extraction that can be consumed from context.Context within MCP handlers.

Changes:

  • Added NewMcpServerWithOptions and option hooks for SDK server options, hooks, and request-based server selection.
  • Added SDK access helpers (SDKServer, RemoveTools) and request metadata extraction + context helpers.
  • Updated docs and expanded mcp package tests to cover the new opt-in features.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
mcp/server.go Adds option-based constructor, server selector path, and request-metadata injection wrapper for HTTP handlers.
mcp/options.go Introduces options API, request metadata types/extractor, and context helper functions.
mcp/helpers.go Adds helpers to expose the underlying SDK server and remove tools at runtime.
mcp/types.go Updates AddTool helper to route through SDKServer instead of relying on concrete type assertions + logging.
mcp/server_test.go Adds test coverage for options, selector behavior, SDK server access, and metadata injection/helpers.
mcp/readme.md Documents advanced configuration, SDK access helpers, dynamic tool updates, and metadata-in-context usage.
mcp/MIGRATION.md Updates migration guidance to reference the new helpers/options and metadata approach.

Comment thread mcp/server.go Outdated

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
metadata := s.metadata(r)
if len(metadata.Path) == 0 {

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

wrapHandler treats an explicitly provided but empty metadata.Path map the same as “not provided” and overwrites it with pathvar.Vars(r) (because it checks len(metadata.Path) == 0). If callers want to deliberately suppress path propagation, they currently can’t. Consider checking metadata.Path == nil (or adding an explicit opt-out flag) so only an omitted Path is auto-filled, while an explicit empty map remains empty.

Suggested change
if len(metadata.Path) == 0 {
if metadata.Path == nil {

Copilot uses AI. Check for mistakes.
Comment thread mcp/options.go
Comment on lines +85 to +90
// HeaderFromContext returns a single extracted header value.
func HeaderFromContext(ctx context.Context, key string) string {
metadata := RequestMetadataFromContext(ctx)
if len(metadata.Headers) == 0 {
return ""
}

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

HeaderFromContext calls RequestMetadataFromContext, which clones all metadata maps before returning. Since this helper only needs a single string, it could read the RequestMetadata directly from ctx.Value(...) (and skip cloning) to avoid per-call allocations; same applies to QueryFromContext and PathFromContext.

Copilot uses AI. Check for mistakes.
Comment thread mcp/types.go
Comment on lines 92 to 96
func AddTool[In, Out any](server McpServer, tool *Tool, handler func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error)) {
// Access internal server - only works with mcpServerImpl
if impl, ok := server.(*mcpServerImpl); ok {
sdkmcp.AddTool(impl.mcpServer, tool, handler)
} else {
logx.Error("AddTool: server must be of type *mcpServerImpl to use this helper")
if sdkServer := SDKServer(server); sdkServer != nil {
sdkmcp.AddTool(sdkServer, tool, handler)
}
}

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

AddTool now silently becomes a no-op when server is not the go-zero implementation (i.e., SDKServer(server) == nil). Previously this logged an error, which helped catch misuse. Either restore a warning/error log, or update the doc comment to explicitly call out that AddTool is a no-op for non-go-zero McpServer implementations so failures aren’t silent.

Copilot uses AI. Check for mistakes.
Comment thread mcp/MIGRATION.md
Comment on lines 9 to 12
Added the official MCP SDK:
```bash
go get github.com/modelcontextprotocol/go-sdk@v1.2.0
go get github.com/modelcontextprotocol/go-sdk@latest
```

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

The migration guide now recommends go get ...@latest, which makes builds non-reproducible and can pull in breaking upstream changes. Prefer documenting a minimum known-good version (or a version range policy) and, if needed, mention that users can choose to upgrade beyond that.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I changed the migration example to pin the SDK version to v1.4.0, which matches the version currently used by the repository.

@kevwan

kevwan commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Review

An excellent, well-documented feature PR that closes #5514 and #5516. The implementation is thoughtful and production-quality.

Architecture:

The PR follows a clean functional options pattern (Option func(*serverOptions)) applied to a new NewMcpServerWithOptions(c, ...Option) constructor, while NewMcpServer(c) becomes a thin wrapper with no options. This is exactly the right approach for backward compatibility.

Feature-by-feature analysis:

Server extension options (options.go)

  • WithServerOptions — passes *sdkmcp.ServerOptions to the underlying SDK constructor. ✅
  • WithServerHook — applies a callback to the *sdkmcp.Server post-construction (for middleware, dynamic tools). Nil-safe. ✅
  • WithServerSelector — allows routing different requests to different SDK server instances. Falls back to the default server when the selector returns nil. ✅

Request metadata (options.go + server.go)

  • RequestMetadata is a simple, copyable value type — no pointers, no sync. ✅
  • wrapHandler correctly short-circuits when s.metadata == nil (zero-overhead path for users not opting in). ✅
  • Defensive cloning in cloneRequestMetadata / cloneStringMap prevents handler mutation of the stored metadata. ✅
  • Auto-populates Path from pathvar.Vars(r) when the extractor doesn't set it explicitly — very practical. ✅
  • Context key uses private struct type (requestMetadataContextKey{}) — correct pattern to avoid key collisions. ✅

SDK access helpers (helpers.go)

  • SDKServer(server) uses type assertion — returns nil for non-go-zero implementations. The docstring makes this explicit. ✅
  • RemoveTools is nil-safe. ✅
  • AddTool in types.go correctly refactored to use the new SDKServer() helper. ✅

Test coverage: The test file is comprehensive (269 additions), covering:

  • NewMcpServerWithOptions hook invocation
  • Server selector fallback behavior
  • RequestMetadata context isolation (mutation after extraction doesn't affect stored value)
  • wrapHandler with/without extractor, with/without path vars
  • Nil context safety

The author claims 100% coverage for mcp/ — impressive.

Minor suggestions (non-blocking):

  1. WithServerHook appends to a slice but callers typically expect a single hook. Multiple hooks is fine, but the naming WithServerHook (singular) might suggest only one is supported. Consider documenting that multiple calls accumulate.

  2. The ServerSelector type returns *sdkmcp.Server but the go-zero MCP server wraps *sdkmcp.Server — if a user accidentally passes a *sdkmcp.Server created outside the selector, it won't have the request metadata context injection. A comment clarifying this would help advanced users.

  3. When the extractor returns an empty RequestMetadata (all nil maps), wrapHandler skips injecting the context value. This means RequestMetadataFromContext returns zero value — consistent and correct, but worth noting in docs for users debugging "why isn't my metadata available."

Overall: This is a high-quality feature contribution. Clean API, correct implementation, thorough tests, and clear documentation. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support opt-in MCP server hooks and route-based server selection Support opt-in request metadata access in MCP handlers

3 participants