logo
1
0
WeChat Login

tRPC MCP Go: Model Context Protocol Implementation with Streaming HTTP Support

A Go implementation of the Model Context Protocol (MCP) with comprehensive streaming HTTP support. This library enables efficient communication between client applications and tools/resources.

Features

Core Features

  • Full MCP Specification Support: Implements MCP, supporting protocol versions up to 2025-03-26 (defaulting to 2024-11-05 for client compatibility in examples).
  • Streaming Support: Real-time data streaming with Server-Sent Events (SSE)
  • Tool Framework: Register and execute tools with structured parameter handling
  • Struct-First API: Generate schemas automatically from Go structs with type safety
  • Resource Management: Serve text and binary resources with RESTful interfaces
  • Prompt Templates: Create and manage prompt templates for LLM interactions
  • Progress Notifications: Built-in support for progress updates on long-running operations
  • Logging System: Integrated logging with configurable levels

Transport Options

trpc-mcp-go supports three transport mechanisms for different deployment scenarios:

STDIO Transport

  • Use Case: Local integrations, command-line tools, cross-language compatibility
  • Communication: Standard input/output streams with JSON-RPC
  • Process Model: Client launches server as subprocess
  • Benefits: Simple, cross-platform, supports TypeScript/Python servers

Streamable HTTP Transport (Recommended)

  • Use Case: Web integrations, multi-client servers, production deployments
  • Communication: HTTP POST requests with optional SSE streaming
  • Features: Session management, resumable connections, scalability
  • Response Modes:
    • JSON responses for simple request-response
    • SSE streaming for real-time operations
  • Server Notifications: Optional GET SSE endpoint for server-initiated messages

SSE Transport (Legacy)

  • Use Case: Backward compatibility with MCP protocol version 2024-11-05
  • Communication: HTTP POST requests + dedicated SSE endpoint for server messages
  • Features: Always stateful with session management and persistent SSE connections
  • Status: Deprecated in favor of Streamable HTTP, maintained for compatibility

Connection Modes

Streamable HTTP Transport supports multiple connection modes:

  • Stateless: Simple request-response pattern without persistent sessions
  • Stateful: Session management with user context and persistent connections

SSE Transport is always stateful by design with persistent connections.

STDIO Transport uses process-based communication (no HTTP sessions).

Installation

go get trpc.group/trpc-go/trpc-mcp-go

Quick Start

Choose Your Transport

Pick the transport that best fits your use case:

TransportWhen to UseExample
STDIOLocal tools, CLI integration, cross-language compatibilityexamples/transport-modes/stdio/
Streamable HTTPWeb services, multi-client servers, production appsexamples/quickstart/
SSE (Legacy)Compatibility with older MCP clientsexamples/transport-modes/sse-legacy/

Streamable HTTP Server Example (Recommended)

package main import ( "context" "fmt" "log" "os" "os/signal" "syscall" mcp "trpc.group/trpc-go/trpc-mcp-go" ) func main() { // Print startup message. log.Printf("Starting basic example server...") // Create server using the new API style: // - First two required parameters: server name and version // - WithServerAddress sets the address to listen on (default: "localhost:3000") // - WithServerPath sets the API path prefix // - WithServerLogger injects logger at the server level mcpServer := mcp.NewServer( "Basic-Example-Server", "0.1.0", mcp.WithServerAddress(":3000"), mcp.WithServerPath("/mcp"), mcp.WithServerLogger(mcp.GetDefaultLogger()), ) // Register basic greet tool. greetTool := mcp.NewTool("greet", mcp.WithDescription("A simple greeting tool."), mcp.WithString("name", mcp.Description("Name to greet."))) greetHandler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Check if the context is cancelled. select { case <-ctx.Done(): return mcp.NewErrorResult("Request cancelled"), ctx.Err() default: // Continue execution. } // Extract name parameter. name := "World" if nameArg, ok := req.Params.Arguments["name"]; ok { if nameStr, ok := nameArg.(string); ok && nameStr != "" { name = nameStr } } // Create greeting message. greeting := fmt.Sprintf("Hello, %s!", name) // Create tool result. return mcp.NewTextResult(greeting), nil } mcpServer.RegisterTool(greetTool, greetHandler) log.Printf("Registered basic greet tool: greet") // Set up a graceful shutdown. stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) // Start server (run in goroutine). go func() { log.Printf("MCP server started, listening on port 3000, path /mcp") if err := mcpServer.Start(); err != nil { log.Fatalf("Server failed to start: %v", err) } }() // Wait for termination signal. <-stop log.Printf("Shutting down server...") }

Streamable HTTP Client Example

package main import ( "context" "fmt" "log" "os" mcp "trpc.group/trpc-go/trpc-mcp-go" ) // initializeClient initializes the MCP client with server connection and session setup func initializeClient(ctx context.Context) (*mcp.Client, error) { log.Println("===== Initialize client =====") serverURL := "http://localhost:3000/mcp" mcpClient, err := mcp.NewClient( serverURL, mcp.Implementation{ Name: "MCP-Go-Client", Version: "1.0.0", }, mcp.WithClientLogger(mcp.GetDefaultLogger()), ) if err != nil { return nil, fmt.Errorf("failed to create client: %v", err) } initResp, err := mcpClient.Initialize(ctx, &mcp.InitializeRequest{}) if err != nil { mcpClient.Close() return nil, fmt.Errorf("initialization failed: %v", err) } log.Printf("Server info: %s %s", initResp.ServerInfo.Name, initResp.ServerInfo.Version) log.Printf("Protocol version: %s", initResp.ProtocolVersion) if initResp.Instructions != "" { log.Printf("Server instructions: %s", initResp.Instructions) } sessionID := mcpClient.GetSessionID() if sessionID != "" { log.Printf("Session ID: %s", sessionID) } return mcpClient, nil } // handleTools manages tool-related operations including listing and calling tools func handleTools(ctx context.Context, client *mcp.Client) error { log.Println("===== List available tools =====") listToolsResp, err := client.ListTools(ctx, &mcp.ListToolsRequest{}) if err != nil { return fmt.Errorf("failed to list tools: %v", err) } tools := listToolsResp.Tools if len(tools) == 0 { log.Printf("No available tools.") return nil } log.Printf("Found %d tools:", len(tools)) for _, tool := range tools { log.Printf("- %s: %s", tool.Name, tool.Description) } // Call the first tool log.Printf("===== Call tool: %s =====", tools[0].Name) callToolReq := &mcp.CallToolRequest{} callToolReq.Params.Name = tools[0].Name callToolReq.Params.Arguments = map[string]interface{}{ "name": "MCP User", } callToolResp, err := client.CallTool(ctx, callToolReq) if err != nil { return fmt.Errorf("failed to call tool: %v", err) } log.Printf("Tool result:") for _, item := range callToolResp.Content { if textContent, ok := item.(mcp.TextContent); ok { log.Printf(" %s", textContent.Text) } } return nil } // terminateSession handles the termination of the current session func terminateSession(ctx context.Context, client *mcp.Client) error { sessionID := client.GetSessionID() if sessionID == "" { return nil } log.Printf("===== Terminate session =====") if err := client.TerminateSession(ctx); err != nil { return fmt.Errorf("failed to terminate session: %v", err) } log.Printf("Session terminated.") return nil } func main() { // Initialize log. log.Println("Starting example client...") // Create context. ctx := context.Background() // Initialize client client, err := initializeClient(ctx) if err != nil { log.Printf("Error: %v\n", err) os.Exit(1) } defer client.Close() // Handle tools if err := handleTools(ctx, client); err != nil { log.Printf("Error: %v\n", err) } // Terminate session if err := terminateSession(ctx, client); err != nil { log.Printf("Error: %v\n", err) } log.Printf("Example finished.") }

STDIO Transport Example

For local integrations and cross-language compatibility:

Server (server/main.go):

func main() { // Create STDIO server mcpServer := mcp.NewStdioServer("My-STDIO-Server", "1.0.0") // Register tools greetTool := mcp.NewTool("greet", mcp.WithDescription("Greet someone"), mcp.WithString("name", mcp.Description("Name to greet"))) mcpServer.RegisterTool(greetTool, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { name := req.Params.Arguments["name"].(string) return mcp.NewTextResult(fmt.Sprintf("Hello, %s!", name)), nil }) // Start STDIO server (reads from stdin, writes to stdout) if err := mcpServer.Start(); err != nil { log.Fatalf("Server failed: %v", err) } }

Client:

func main() { // Create STDIO client that launches server as subprocess client, err := mcp.NewStdioClient( mcp.StdioTransportConfig{ ServerParams: mcp.StdioServerParameters{ Command: "go", Args: []string{"run", "./server/main.go"}, }, }, mcp.Implementation{Name: "My-Client", Version: "1.0.0"}, ) if err != nil { log.Fatal(err) } defer client.Close() // Initialize and use the client ctx := context.Background() if _, err := client.Initialize(ctx, &mcp.InitializeRequest{}); err != nil { log.Fatal(err) } // Call tools result, err := client.CallTool(ctx, &mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: "greet", Arguments: map[string]interface{}{"name": "World"}, }, }) if err != nil { log.Fatal(err) } fmt.Printf("Result: %v\n", result.Content[0]) }

Configuration

Server Configuration

The server can be configured using option functions:

server := mcp.NewServer( "My-MCP-Server", // Server name "1.0.0", // Server version mcp.WithServerAddress(":3000"), // Listen address mcp.WithServerPath("/mcp"), // API path prefix mcp.WithPostSSEEnabled(true), // Enable SSE responses mcp.WithGetSSEEnabled(true), // Allow GET for SSE mcp.WithStatelessMode(false), // Use stateful mode mcp.WithServerLogger(mcp.GetDefaultLogger()), // Custom logger )

Available Server Options

OptionDescriptionDefault
WithServerAddressSet server address to listen on"localhost:3000"
WithServerPathSet API path prefix/mcp
WithServerLoggerCustom logger for serverDefault logger
WithoutSessionDisable session managementSessions enabled
WithPostSSEEnabledEnable SSE responsestrue
WithGetSSEEnabledAllow GET for SSE connectionstrue
WithNotificationBufferSizeSize of notification buffer10
WithStatelessModeRun in stateless modefalse

Client Configuration

The client can be configured using option functions:

client, err := mcp.NewClient( "http://localhost:3000/mcp", // Server URL mcp.Implementation{ // Client info Name: "MCP-Client", Version: "1.0.0", }, mcp.WithProtocolVersion(mcp.ProtocolVersion_2025_03_26), // Protocol version mcp.WithClientGetSSEEnabled(true), // Use GET for SSE mcp.WithClientLogger(mcp.GetDefaultLogger()), // Custom logger )

Available Client Options

OptionDescriptionDefault
WithProtocolVersionSpecify MCP protocol versionmcp.ProtocolVersion_2025_03_26
WithClientGetSSEEnabledUse GET for SSE instead of POSTtrue
WithClientLoggerCustom logger for clientDefault logger
WithClientPathSet custom client pathServer path
WithHTTPReqHandlerUse custom HTTP request handlerDefault handler

Advanced Features

Streaming Progress with SSE

Create tools that provide real-time progress updates:

// handleMultiStageGreeting handles the multi-stage greeting tool and sends multiple notifications via SSE. func handleMultiStageGreeting(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract name from parameters. name := "Guest" if nameArg, ok := req.Params.Arguments["name"]; ok { if nameStr, ok := nameArg.(string); ok && nameStr != "" { name = nameStr } } stages := 3 if stagesArg, ok := req.Params.Arguments["stages"]; ok { if stagesFloat, ok := stagesArg.(float64); ok && stagesFloat > 0 { stages = int(stagesFloat) } } // Get notification sender from context. notificationSender, hasNotificationSender := mcp.GetNotificationSender(ctx) if !hasNotificationSender { log.Printf("unable to get notification sender from context") return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.NewTextContent("Error: unable to get notification sender."), }, }, fmt.Errorf("unable to get notification sender from context") } // Send progress update. sendProgress := func(progress float64, message string) { err := notificationSender.SendProgress(progress, message) if err != nil { log.Printf("Failed to send progress notification: %v", err) } } // Send log message. sendLogMessage := func(level string, message string) { err := notificationSender.SendLogMessage(level, message) if err != nil { log.Printf("Failed to send log notification: %v", err) } } // Start greeting process. sendProgress(0.0, "Start multi-stage greeting") sendLogMessage("info", fmt.Sprintf("Start greeting to %s", name)) time.Sleep(500 * time.Millisecond) // Send multiple stage notifications. for i := 1; i <= stages; i++ { // Check if context is canceled. select { case <-ctx.Done(): return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.NewTextContent(fmt.Sprintf("Greeting canceled at stage %d", i)), }, }, ctx.Err() default: // Continue sending. } sendProgress(float64(i)/float64(stages), fmt.Sprintf("Stage %d greeting", i)) sendLogMessage("info", fmt.Sprintf("Stage %d greeting: Hello %s!", i, name)) time.Sleep(800 * time.Millisecond) } // Send final greeting. sendProgress(1.0, "Greeting completed") sendLogMessage("info", fmt.Sprintf("Completed multi-stage greeting to %s", name)) // Return final result. return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.NewTextContent(fmt.Sprintf( "Completed %d-stage greeting to %s!", stages, name, )), }, }, nil } func main() { // Create MCP server mcpServer := mcp.NewServer( "Multi-Stage-Greeting-Server", "1.0.0", mcp.WithServerAddress(":3000"), mcp.WithServerPath("/mcp"), mcp.WithPostSSEEnabled(true), ) // Register a multi-stage greeting tool multiStageGreetingTool := mcp.NewTool("multi-stage-greeting", mcp.WithDescription("Send multi-stage greeting via SSE."), mcp.WithString("name", mcp.Description("Name to greet.")), mcp.WithNumber("stages", mcp.Description("Number of greeting stages."), mcp.Default(3), ), ) mcpServer.RegisterTool(multiStageGreetingTool, handleMultiStageGreeting) log.Printf("Registered multi-stage greeting tool: multi-stage-greeting") // Start server log.Printf("MCP server started at :3000, path /mcp") if err := mcpServer.Start(); err != nil { log.Fatalf("Failed to start server: %v", err) } }

Client-Side Progress Handling

package main import ( "context" "fmt" "log" "trpc.group/trpc-go/trpc-mcp-go" ) // Example NotificationCollector structure and methods type NotificationCollector struct{} func (nc *NotificationCollector) HandleProgress(notification *mcp.JSONRPCNotification) error { progress, _ := notification.Params.AdditionalFields["progress"].(float64) message, _ := notification.Params.AdditionalFields["message"].(string) fmt.Printf("Progress: %.0f%% - %s\n", progress*100, message) return nil } func (nc *NotificationCollector) HandleLog(notification *mcp.JSONRPCNotification) error { level, _ := notification.Params.AdditionalFields["level"].(string) data, _ := notification.Params.AdditionalFields["data"].(string) fmt.Printf("[%s] %s\n", level, data) return nil } func main() { // Create context ctx := context.Background() // Initialize client client, err := mcp.NewClient( "http://localhost:3000/mcp", mcp.Implementation{ Name: "MCP-Client-Stream-Handler", Version: "1.0.0", }, mcp.WithClientGetSSEEnabled(true), ) if err != nil { log.Fatalf("Failed to create client: %v", err) } defer client.Close() // Initialize connection _, err = client.Initialize(ctx, &mcp.InitializeRequest{}) if err != nil { log.Fatalf("Failed to initialize client: %v", err) } // Create notification collector collector := &NotificationCollector{} // Register notification handlers client.RegisterNotificationHandler("notifications/progress", collector.HandleProgress) client.RegisterNotificationHandler("notifications/message", collector.HandleLog) // Call tool with streaming log.Printf("Calling multi-stage greeting tool...") callRes, err := client.CallTool(ctx, &mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: "multi-stage-greeting", Arguments: map[string]interface{}{ "name": "MCP User", "stages": 5, }, }, }) if err != nil { log.Printf("Tool call failed: %v", err) return } // Process final result for _, item := range callRes.Content { if textContent, ok := item.(mcp.TextContent); ok { log.Printf("Final tool result: %s", textContent.Text) } } log.Printf("Client example finished.") }

Resource Management

Single Content Resources

Register and serve resources with single content:

// Register text resource textResource := &mcp.Resource{ URI: "resource://example/text", Name: "example-text", Description: "Example text resource", MimeType: "text/plain", } // Define text resource handler textHandler := func(ctx context.Context, req *mcp.ReadResourceRequest) (mcp.ResourceContents, error) { return mcp.TextResourceContents{ URI: textResource.URI, MIMEType: textResource.MimeType, Text: "This is an example text resource content.", }, nil } // Register the text resource server.RegisterResource(textResource, textHandler)

Multiple Contents Resources (Recommended)

Register resources that can provide multiple content representations:

// Register a resource with multiple content formats multiResource := &mcp.Resource{ URI: "resource://example/document", Name: "example-document", Description: "Document available in multiple formats", MimeType: "text/plain", } // Define multiple contents handler multiHandler := func(ctx context.Context, req *mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return []mcp.ResourceContents{ // Text representation mcp.TextResourceContents{ URI: req.Params.URI, MIMEType: "text/plain", Text: "Document content in plain text format.", }, // JSON representation mcp.BlobResourceContents{ URI: req.Params.URI, MIMEType: "application/json", Blob: "eyJjb250ZW50IjogIkRvY3VtZW50IGNvbnRlbnQgaW4gSlNPTiBmb3JtYXQuIn0=", // base64 encoded JSON }, }, nil } // Register the resource with multiple contents (recommended) server.RegisterResources(multiResource, multiHandler)

The RegisterResources method is recommended as it:

  • Aligns with the MCP protocol specification
  • Allows a single resource to provide multiple content representations
  • Supports use cases like serving the same data in different formats (text, JSON, XML, etc.)

Prompt

Register prompt:

// Register basic prompt basicPrompt := &mcp.Prompt{ Name: "basic-prompt", Description: "Basic prompt example", Arguments: []mcp.PromptArgument{ { Name: "name", Description: "User name", Required: true, }, }, } // Define basic prompt handler basicPromptHandler := func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { name := req.Params.Arguments["name"] return &mcp.GetPromptResult{ Description: basicPrompt.Description, Messages: []mcp.PromptMessage{ { Role: "user", Content: mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Hello, %s! This is a basic prompt example.", name), }, }, }, }, nil } // Register the basic prompt server.RegisterPrompt(basicPrompt, basicPromptHandler)

Struct-First API (Recommended)

Define MCP tools using Go structs for automatic schema generation and type safety:

// Define input/output structures type WeatherInput struct { Location string `json:"location" jsonschema:"required,description=City name"` Units string `json:"units,omitempty" jsonschema:"description=Temperature units,enum=celsius,enum=fahrenheit,default=celsius"` } type WeatherOutput struct { Temperature float64 `json:"temperature" jsonschema:"description=Current temperature"` Description string `json:"description" jsonschema:"description=Weather description"` } // Create tool with automatic schema generation weatherTool := mcp.NewTool( "get_weather", mcp.WithDescription("Get weather information"), mcp.WithInputStruct[WeatherInput](), // 🚀 Auto-generate input schema mcp.WithOutputStruct[WeatherOutput](), // 🚀 Auto-generate output schema ) // Type-safe handler with automatic validation weatherHandler := mcp.NewTypedToolHandler(func(ctx context.Context, req *mcp.CallToolRequest, input WeatherInput) (WeatherOutput, error) { return WeatherOutput{ Temperature: 22.5, Description: "Partly cloudy", }, nil })

Schema Generation Styles:

By default, schemas use $defs + $ref for compact representation (~2KB). You can customize the generation style:

// Ref style (default) - compact schemas with $defs + $ref mcp.WithInputStruct[Input](mcp.WithRefStyle()) // Inline style - expand all types inline (larger schemas) mcp.WithOutputStruct[Output](mcp.WithInlineStyle())

Benefits:

  • 🛡️ Type Safety: Compile-time type checking and automatic validation
  • 📋 Rich Schemas: Auto-generated OpenAPI schemas with descriptions, enums, defaults
  • 🔄 Structured Output: Type-safe responses with backward compatibility
  • 🎯 DRY Principle: Single source of truth for data structures
  • 🎨 Flexible Styles: Choose between compact ($ref) or expanded (inline) schemas

See examples/schema-generation/ for a complete example.

Example Patterns

The project includes several example patterns organized by category:

Getting Started

PatternDescription
quickstart/Quick Start - Simple tool registration and basic MCP usage

Transport Modes

PatternDescription
transport-modes/stdio/STDIO Transport - Cross-language compatibility with stdio
transport-modes/sse-legacy/SSE Transport - Server-Sent Events (legacy, for compatibility)
transport-modes/streamable-http/Streamable HTTP - Various configuration options

⚙Streamable HTTP Configurations

PatternResponse ModeGET SSESessionDescription
stateless-json/JSON ResponseSimple request-response
stateful-json/JSON ResponseJSON with session management
stateless-sse/SSE StreamingReal-time streaming responses
stateful-sse/SSE StreamingStreaming with session management
stateful-json-getsse/JSON ResponseJSON + server notifications
stateful-sse-getsse/SSE StreamingFull-featured streaming

Advanced Features

PatternDescription
schema-generation/Struct-first API - Auto-generate schemas from Go structs
resources-and-prompts/Resources & Prompts - Resource management and prompt templates
roots-management/Roots Management - Client filesystem integration across transports

FAQ

1. How to handle HTTP Headers?

Q: How can I extract HTTP headers on the server side and send custom headers from the client?

A: The library provides comprehensive HTTP header support for both server and client sides while maintaining transport layer independence.

Server Side: Extracting HTTP Headers

Use WithHTTPContextFunc to extract HTTP headers and make them available in tool handlers through context:

package main import ( "context" "fmt" "log" "net/http" mcp "trpc.group/trpc-go/trpc-mcp-go" ) // Define context keys for type safety type contextKey string const ( AuthTokenKey contextKey = "auth_token" UserAgentKey contextKey = "user_agent" ) // HTTP context functions to extract headers func extractAuthToken(ctx context.Context, r *http.Request) context.Context { if authHeader := r.Header.Get("Authorization"); authHeader != "" { return context.WithValue(ctx, AuthTokenKey, authHeader) } return ctx } func extractUserAgent(ctx context.Context, r *http.Request) context.Context { if userAgent := r.Header.Get("User-Agent"); userAgent != "" { return context.WithValue(ctx, UserAgentKey, userAgent) } return ctx } // Tool handler that accesses headers via context func myTool(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Extract headers from context authToken, _ := ctx.Value(AuthTokenKey).(string) userAgent, _ := ctx.Value(UserAgentKey).(string) // Use header information in your tool logic response := fmt.Sprintf("Received auth: %s, user-agent: %s", authToken, userAgent) return mcp.NewTextResult(response), nil } func main() { // Create server with HTTP context functions server := mcp.NewServer( "header-server", "1.0.0", // Register multiple HTTP context functions mcp.WithHTTPContextFunc(extractAuthToken), mcp.WithHTTPContextFunc(extractUserAgent), ) // Register tool tool := mcp.NewTool("my-tool", mcp.WithDescription("Tool that uses headers")) server.RegisterTool(tool, myTool) // Start server server.Start() }

Client Side: Sending Custom HTTP Headers

Option 1: Static Headers with WithHTTPHeaders

Use WithHTTPHeaders for headers that don't change across requests:

// Create custom headers headers := make(http.Header) headers.Set("Authorization", "Bearer your-token-here") headers.Set("User-Agent", "MyMCPClient/1.0") // Create client with static headers client, err := mcp.NewClient( "http://localhost:3000/mcp", mcp.Implementation{Name: "my-client", Version: "1.0.0"}, mcp.WithHTTPHeaders(headers), // Applied to all requests )

Option 2: Dynamic Headers with WithHTTPBeforeRequest

Use WithHTTPBeforeRequest for headers that change per request (e.g., per-request auth tokens, request IDs, tracing):

// Define context keys type contextKey string const ( requestIDKey contextKey = "request-id" userIDKey contextKey = "user-id" ) // Create before-request function beforeRequest := func(ctx context.Context, req *http.Request) error { // Extract values from context and set headers if requestID, ok := ctx.Value(requestIDKey).(string); ok { req.Header.Set("X-Request-ID", requestID) } if userID, ok := ctx.Value(userIDKey).(string); ok { req.Header.Set("X-User-ID", userID) } return nil // Return error to abort the request } // Create client with dynamic headers client, err := mcp.NewClient( "http://localhost:3000/mcp", mcp.Implementation{Name: "my-client", Version: "1.0.0"}, mcp.WithHTTPBeforeRequest(beforeRequest), ) // Pass context with values for each request ctx := context.WithValue(context.Background(), requestIDKey, "req-12345") ctx = context.WithValue(ctx, userIDKey, "user-789") // Headers are dynamically set from context result, err := client.CallTool(ctx, &mcp.CallToolRequest{ Params: mcp.CallToolParams{ Name: "my-tool", Arguments: map[string]interface{}{"message": "Hello!"}, }, })

Key Differences:

FeatureWithHTTPHeadersWithHTTPBeforeRequest
Use CaseStatic headers (API keys, service name)Dynamic headers (request ID, user context)
When SetOnce at client creationBefore each HTTP request
Context Access❌ No✅ Yes - read from context.Context
Applies ToAll requestsAll requests (initialize, tools/list, tools/call, GET SSE)
Can Abort Request❌ No✅ Yes - return error

Combining Both:

// Static headers for service identification headers := make(http.Header) headers.Set("X-Service-Name", "my-service") // Dynamic headers for per-request data beforeRequest := func(ctx context.Context, req *http.Request) error { if requestID, ok := ctx.Value(requestIDKey).(string); ok { req.Header.Set("X-Request-ID", requestID) } return nil } client, err := mcp.NewClient( serverURL, clientInfo, mcp.WithHTTPHeaders(headers), // Static headers mcp.WithHTTPBeforeRequest(beforeRequest), // Dynamic headers ) // Both are applied: static headers first, then dynamic headers

Key Features

  • Transport Layer Independence: Tool handlers access headers through context, not directly from HTTP requests
  • Multiple Context Functions: Use multiple WithHTTPContextFunc calls to extract different headers
  • Type Safety: Use strongly-typed context keys to avoid conflicts
  • Static + Dynamic Headers: Combine WithHTTPHeaders and WithHTTPBeforeRequest for maximum flexibility
  • Context Propagation: WithHTTPBeforeRequest enables per-request headers via context.Context
  • All Requests Covered: Headers apply to initialize, tools/list, tools/call, and GET SSE connections
  • Backward Compatibility: Optional features that don't break existing APIs

Complete Example

See examples/quickstart/ for basic usage, or examples/transport-modes/ for transport-specific header handling.

2. How to get server name and version in a tool handler?

You can retrieve the mcp.Server instance from the context.Context within your tool handler and then call its GetServerInfo() method:

func handleMyTool(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { serverInstance := mcp.GetServerFromContext(ctx) if mcpServer, ok := serverInstance.(*mcp.Server); ok { serverInfo := mcpServer.GetServerInfo() // Now you have serverInfo.Name and serverInfo.Version return mcp.NewTextResult(fmt.Sprintf("Server Name: %s, Version: %s", serverInfo.Name, serverInfo.Version)), nil } return mcp.NewTextResult("Could not retrieve server information."), nil }

3. How to dynamically filter tools, prompts, and resources based on user context?

The library provides a powerful filtering system that works with HTTP header extraction to enable dynamic access control. You can filter tools, prompts, and resources based on user context such as roles, permissions, client version, or any other criteria.

Basic Context-Based Filtering

Use filtering options combined with WithHTTPContextFunc to filter tools, prompts, and resources based on user context (e.g., roles, permissions, version, etc.):

package main import ( "context" "net/http" mcp "trpc.group/trpc-go/trpc-mcp-go" ) // Extract user role from HTTP headers func extractUserRole(ctx context.Context, r *http.Request) context.Context { if userRole := r.Header.Get("X-User-Role"); userRole != "" { ctx = context.WithValue(ctx, "user_role", userRole) } return ctx } // Create role-based filter for tools func createRoleBasedFilter() mcp.ToolListFilter { return func(ctx context.Context, tools []*mcp.Tool) []*mcp.Tool { userRole := "guest" // default if role, ok := ctx.Value("user_role").(string); ok && role != "" { userRole = role } var filtered []*mcp.Tool for _, tool := range tools { switch userRole { case "admin": filtered = append(filtered, tool) // Admin sees all tools case "user": if tool.Name == "calculator" || tool.Name == "weather" { filtered = append(filtered, tool) } case "guest": if tool.Name == "calculator" { filtered = append(filtered, tool) } } } return filtered } } // Create role-based filter for prompts func createPromptFilter() mcp.PromptListFilter { return func(ctx context.Context, prompts []*mcp.Prompt) []*mcp.Prompt { userRole := "guest" // default if role, ok := ctx.Value("user_role").(string); ok && role != "" { userRole = role } var filtered []*mcp.Prompt for _, prompt := range prompts { switch userRole { case "admin": filtered = append(filtered, prompt) // Admin sees all prompts case "user": if prompt.Name == "greeting" || prompt.Name == "summary" { filtered = append(filtered, prompt) } case "guest": if prompt.Name == "greeting" { filtered = append(filtered, prompt) } } } return filtered } } // Create role-based filter for resources func createResourceFilter() mcp.ResourceListFilter { return func(ctx context.Context, resources []*mcp.Resource) []*mcp.Resource { userRole := "guest" // default if role, ok := ctx.Value("user_role").(string); ok && role != "" { userRole = role } var filtered []*mcp.Resource for _, resource := range resources { switch userRole { case "admin": filtered = append(filtered, resource) // Admin sees all resources case "user": if resource.Name == "public_data" || resource.Name == "user_data" { filtered = append(filtered, resource) } case "guest": if resource.Name == "public_data" { filtered = append(filtered, resource) } } } return filtered } } func main() { // Create server with context-based filtering for all types server := mcp.NewServer( "filtered-server", "1.0.0", mcp.WithHTTPContextFunc(extractUserRole), mcp.WithToolListFilter(createRoleBasedFilter()), mcp.WithPromptListFilter(createPromptFilter()), mcp.WithResourceListFilter(createResourceFilter()), ) // Register tools, prompts, and resources as usual server.RegisterTool(calculatorTool, calculatorHandler) server.RegisterTool(weatherTool, weatherHandler) server.RegisterTool(adminTool, adminHandler) server.RegisterPrompt(userPrompt, userPromptHandler) server.RegisterPrompt(adminPrompt, adminPromptHandler) server.RegisterResource(publicResource, publicResourceHandler) server.RegisterResource(adminResource, adminResourceHandler) server.Start() }

Advanced Custom Filter Functions

You can create more complex filtering logic:

func createAdvancedFilter() mcp.ToolListFilter { return func(ctx context.Context, tools []*mcp.Tool) []*mcp.Tool { // Extract user information from context userRole := "guest" if role, ok := ctx.Value("user_role").(string); ok && role != "" { userRole = role } clientVersion := "" if version, ok := ctx.Value("client_version").(string); ok { clientVersion = version } // Complex business logic if userRole == "premium" && clientVersion == "2.0.0" { return tools // Premium users with v2.0.0 see all tools } var filtered []*mcp.Tool for _, tool := range tools { if shouldShowTool(tool, userRole, clientVersion) { filtered = append(filtered, tool) } } return filtered } } // Helper function for business logic func shouldShowTool(tool *mcp.Tool, userRole, clientVersion string) bool { // Implement your business logic here switch tool.Name { case "calculator": return true // Available to everyone case "weather": return userRole == "user" || userRole == "admin" case "admin_panel": return userRole == "admin" default: return false } }

SSE Server Filtering

For SSE servers, use the corresponding SSE filter options:

// Create SSE server with filtering sseServer := mcp.NewSSEServer( "filtered-sse-server", "1.0.0", mcp.WithSSEContextFunc(extractUserRole), mcp.WithSSEToolListFilter(createRoleBasedFilter()), mcp.WithSSEPromptListFilter(createPromptFilter()), mcp.WithSSEResourceListFilter(createResourceFilter()), )

Testing Filtering

You can test different user contexts by sending HTTP headers:

# Admin sees all tools curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "X-User-Role: admin" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' # Test prompts filtering curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "X-User-Role: user" \ -d '{"jsonrpc": "2.0", "id": 2, "method": "prompts/list"}' # Test resources filtering curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "X-User-Role: guest" \ -d '{"jsonrpc": "2.0", "id": 3, "method": "resources/list"}' # User sees filtered tools curl -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "X-User-Role: user" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'

Key Benefits

  • Flexible Filtering: Create custom filters based on any context information
  • Transport Independence: Works with both stateful and stateless modes
  • Performance: Filters run only when tools are listed, not on every request
  • Security: Tools are completely hidden from unauthorized users

4. How to configure retry mechanism for network errors?

The library provides automatic retry functionality at the transport layer to handle temporary network failures.

Simple Retry Configuration

// Enable retry with 3 attempts (recommended for most use cases) client, err := mcp.NewClient(serverURL, clientInfo, mcp.WithSimpleRetry(3), )

Advanced Retry Configuration

// Custom retry configuration for specific scenarios client, err := mcp.NewClient(serverURL, clientInfo, mcp.WithRetry(mcp.RetryConfig{ MaxRetries: 5, // Maximum retry attempts InitialBackoff: 1 * time.Second, // Initial delay before first retry BackoffFactor: 1.5, // Exponential backoff multiplier MaxBackoff: 15 * time.Second, // Maximum delay cap }), )

What Errors Are Retried?

The retry mechanism automatically handles:

  • Connection errors: connection refused, connection reset, connection timeout
  • I/O timeouts: i/o timeout, read timeout, dial timeout
  • Network errors: EOF, broken pipe
  • HTTP server errors: Status codes 408, 409, 429, and all 5xx errors

Key Features

  • Transport Layer: Retry works across all MCP operations (tools, resources, prompts)
  • Exponential Backoff: Intelligent delay strategy to avoid overwhelming servers
  • Error Classification: Only retries temporary failures, not permanent errors (auth, bad requests)
  • Silent Operation: No logging noise by default, retry happens transparently
  • Network Transports: Works with Streamable HTTP and SSE transports (STDIO doesn't use retry due to its process-based nature)

5. How to use tool annotations for better UX?

Tool annotations provide behavioral hints to MCP clients about tool characteristics, enabling smarter usage and better user experience:

tool := mcp.NewTool("weather_api", mcp.WithDescription("Get weather information"), mcp.WithString("location", mcp.Description("City name"), mcp.Required()), mcp.WithToolAnnotations(&mcp.ToolAnnotations{ Title: "Weather Information", // Human-readable title ReadOnlyHint: mcp.BoolPtr(true), // Safe, no side effects DestructiveHint: mcp.BoolPtr(false), // Non-destructive operation IdempotentHint: mcp.BoolPtr(true), // Same input → same output OpenWorldHint: mcp.BoolPtr(true), // Interacts with external APIs }), )

Key annotation types:

  • ReadOnlyHint: Tool doesn't modify environment (safe for repeated calls)
  • DestructiveHint: Tool may cause permanent changes (only meaningful when ReadOnlyHint is false)
  • IdempotentHint: Multiple calls with same arguments have same effect
  • OpenWorldHint: Tool interacts with external systems vs internal data

Note: Annotations are hints for UX optimization, not security guarantees. See examples/annotations/ for comprehensive usage patterns.

6. How to get registered tools from server?

Q: How can I check what tools are currently registered on the server before performing operations?

A: Use the GetTool and GetTools methods available on all server types:

// Check if a specific tool exists if tool, exists := server.GetTool("weather_api"); exists { fmt.Printf("Tool found: %s - %s\n", tool.Name, tool.Description) // Safe to proceed with tool-related operations } // Get all registered tools tools := server.GetTools() fmt.Printf("Server has %d tools registered:\n", len(tools)) for _, tool := range tools { fmt.Printf("- %s: %s\n", tool.Name, tool.Description) }

Key features:

  • Safe Access: Returns tool copies to prevent accidental modification
  • Universal Support: Available on Server, SSEServer, and StdioServer
  • Dynamic Management: Perfect for conditional tool registration/removal
  • Go Idioms: Uses (value, bool) pattern for existence checking

7. How to use Middleware to intercept and modify requests/responses?

Q: How can I add custom logic before/after tool execution, intercept specific requests, or modify results?

A: The library provides a powerful middleware system that allows you to intercept and modify any MCP request/response. Middleware is especially useful for:

  • Tool/Prompt interception: Mock, cache, or block specific tools/prompts
  • Result modification: Transform or enhance responses
  • Custom result construction: Return completely custom results without calling the actual handler
  • Cross-cutting concerns: Logging, metrics, authentication, tracing

Basic Middleware Pattern

Middleware follows a simple function signature:

type Middleware func(next HandlerFunc) HandlerFunc type HandlerFunc func(ctx context.Context, req *JSONRPCRequest) (JSONRPCMessage, error)

Register middleware using WithMiddleware option:

server := mcp.NewServer( "my-server", "1.0.0", mcp.WithMiddleware( LoggingMiddleware, MetricsMiddleware, AuthMiddleware, ToolInterceptorMiddleware, ), )

Middleware executes in onion model (first registered = outer layer):

Request → Logging → Metrics → Auth → Tool Interceptor → Handler ↓ Response ← Logging ← Metrics ← Auth ← Tool Interceptor ← Handler

Example 1: Intercepting and Mocking Tool Calls

// ToolInterceptorMiddleware intercepts specific tools and returns mock results func ToolInterceptorMiddleware(next mcp.HandlerFunc) mcp.HandlerFunc { return func(ctx context.Context, req *mcp.JSONRPCRequest) (mcp.JSONRPCMessage, error) { // Only intercept tool call requests if req.Method != mcp.MethodToolsCall { return next(ctx, req) } // Parse tool name from request var callReq mcp.CallToolRequest if params, ok := req.Params.(map[string]interface{}); ok { if name, ok := params["name"].(string); ok { callReq.Params.Name = name } } // Intercept specific tool and return mock result if callReq.Params.Name == "expensive-api" { log.Printf("🔄 Mocking 'expensive-api' tool, actual handler not called") // Construct and return mock result directly mockResult := mcp.NewTextResult("Mock response from middleware!") return mockResult, nil // ← Handler is NOT called } // For other tools, continue to actual handler return next(ctx, req) } }

Key Point: When you return a result directly, the actual tool handler is bypassed, saving execution time and resources.

Example 2: Intercepting and Enhancing Prompt Lists

// PromptInterceptorMiddleware adds dynamic prompts to the list func PromptInterceptorMiddleware(next mcp.HandlerFunc) mcp.HandlerFunc { return func(ctx context.Context, req *mcp.JSONRPCRequest) (mcp.JSONRPCMessage, error) { if req.Method != mcp.MethodPromptsList { return next(ctx, req) } // Call original handler to get base prompt list result, err := next(ctx, req) if err != nil { return nil, err } // Type assert and modify the result if promptList, ok := result.(*mcp.ListPromptsResult); ok { // Add a dynamic prompt generated by middleware promptList.Prompts = append(promptList.Prompts, mcp.Prompt{ Name: "dynamic-prompt", Description: "🎯 Dynamically added by middleware!", Arguments: []mcp.PromptArgument{ { Name: "topic", Description: "Topic to discuss", Required: false, }, }, }) log.Printf("✅ Added 1 dynamic prompt to the list") } return result, err } }

Example 3: Intercepting prompts/get and Returning Cached Content

func PromptCacheMiddleware(next mcp.HandlerFunc) mcp.HandlerFunc { return func(ctx context.Context, req *mcp.JSONRPCRequest) (mcp.JSONRPCMessage, error) { if req.Method != mcp.MethodPromptsGet { return next(ctx, req) } // Parse prompt name var getPromptReq mcp.GetPromptRequest if params, ok := req.Params.(map[string]interface{}); ok { if name, ok := params["name"].(string); ok { getPromptReq.Params.Name = name } } // Check cache for specific prompt if getPromptReq.Params.Name == "cached-prompt" { log.Printf("💾 Returning cached prompt content") // Construct cached result directly cachedResult := &mcp.GetPromptResult{ Description: "💾 Served from cache", Messages: []mcp.PromptMessage{ { Role: mcp.RoleUser, Content: mcp.TextContent{ Type: "text", Text: "This is cached content, loaded instantly!", }, }, }, } return cachedResult, nil // ← Handler is NOT called } // For uncached prompts, call actual handler return next(ctx, req) } }

Example 4: Result Construction - All MCP Method Types

Middleware can construct results for any MCP method by returning the appropriate result type:

func InterceptorMiddleware(next mcp.HandlerFunc) mcp.HandlerFunc { return func(ctx context.Context, req *mcp.JSONRPCRequest) (mcp.JSONRPCMessage, error) { switch req.Method { case mcp.MethodToolsCall: // Return CallToolResult return &mcp.CallToolResult{ Content: []mcp.Content{ {Type: "text", Text: "Custom tool result"}, }, }, nil case mcp.MethodPromptsGet: // Return GetPromptResult return &mcp.GetPromptResult{ Description: "Custom prompt", Messages: []mcp.PromptMessage{ { Role: mcp.RoleUser, Content: mcp.TextContent{ Type: "text", Text: "Custom prompt content", }, }, }, }, nil case mcp.MethodPromptsList: // Return ListPromptsResult return &mcp.ListPromptsResult{ Prompts: []mcp.Prompt{ {Name: "custom-prompt", Description: "Added by middleware"}, }, }, nil case mcp.MethodResourcesRead: // Return ReadResourceResult return &mcp.ReadResourceResult{ Contents: []mcp.ResourceContents{ { URI: "custom://resource", MimeType: "text/plain", Text: "Custom resource content", }, }, }, nil case mcp.MethodInitialize: // Modify InitializeResult by calling handler first result, err := next(ctx, req) if err != nil { return nil, err } if initResult, ok := result.(*mcp.InitializeResult); ok { initResult.Instructions = "🎯 Enhanced by middleware!\n" + initResult.Instructions } return result, err default: return next(ctx, req) } } }

Common Use Cases

1. Tool Mocking/Testing

if toolName == "external-api" { // Return mock data for testing return mcp.NewTextResult("Mock API response"), nil }

2. Tool Caching

if cached := cache.Get(toolName + args); cached != nil { return cached, nil // Return from cache } result, err := next(ctx, req) // Call handler cache.Set(key, result) // Cache result return result, err

3. Graceful Degradation

if toolName == "unreliable-service" { // Return fallback response instead of calling unstable service return mcp.NewTextResult("Service unavailable, using fallback"), nil }

4. Access Control

if toolName == "admin-tool" { session := mcp.ClientSessionFromContext(ctx) if !isAdmin(session) { result := mcp.NewTextResult("Access denied") result.IsError = true return result, nil } }

5. Dynamic Prompt Loading

if req.Method == mcp.MethodPromptsGet { // Load prompt from external source (DB, file, API) promptContent := loadFromDatabase(promptName) return &mcp.GetPromptResult{ Messages: []mcp.PromptMessage{{ Role: mcp.RoleUser, Content: mcp.TextContent{Text: promptContent}, }}, }, nil }

Complete Example

See examples/middleware/ for a comprehensive demonstration including:

  • ✅ Trace IDs
  • ✅ Request logging
  • ✅ Metrics collection
  • ✅ Authorization checking
  • ✅ Tool interception (mock/cache/degrade/block)
  • ✅ Prompt interception (dynamic addition/caching)
  • ✅ Result modification and construction

Run the example:

# Terminal 1: Start server cd examples/middleware/server go build && ./middleware-server # Terminal 2: Run client cd examples/middleware/client go build && ./client

Key Takeaways:

  • Middleware operates at the JSON-RPC layer, giving you full control over all MCP methods
  • You can intercept any MCP request: tools, prompts, resources, initialize, ping, etc.
  • Return results directly to bypass the actual handler (useful for mocking, caching, blocking)
  • Call next(ctx, req) first, then modify the result for enhancement scenarios
  • Use proper result types: *CallToolResult, *GetPromptResult, *ListPromptsResult, etc.
  • Middleware executes in registration order (onion model)

Copyright

The copyright notice pertaining to the Tencent code in this repo was previously in the name of “THL A29 Limited.” That entity has now been de-registered. You should treat all previously distributed copies of the code as if the copyright notice was in the name of “Tencent.”

About

No description, topics, or website provided.
1005.00 KiB
1 forks0 stars2 branches0 TagREADMEOther license
Language
Go99.8%
Dockerfile0.2%